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

Compare commits

...

152 Commits

Author SHA1 Message Date
Wim
858e16d34f Release v1.14.1 2019-03-21 21:07:11 +01:00
Wim
a60e62efb1 Update doc wrt rocketchat api issue 2019-03-21 21:05:27 +01:00
97f9d4be67 Fix double unlock (slack) (#771) 2019-03-21 17:30:28 +01:00
Wim
fa4eec41f7 Release v1.14.0 2019-03-20 23:30:03 +01:00
Wim
77516c97db Allow the # in rocketchat channels (backward compatible) (#769) 2019-03-20 23:19:27 +01:00
Wim
cba01f0865 Update rocketchat documentation 2019-03-20 23:18:40 +01:00
8b754017ca Fix race-condition in populateUser() (#767)
Fix the root-cause of #759 by introducing synchronisation points for
individual user fetches.
2019-03-20 22:54:31 +01:00
Wim
a27600046e Fix regression for legacy slack by #766 (#768) 2019-03-20 22:52:23 +01:00
fb2667631d Refactor channel and user management (slack) (#766) 2019-03-15 21:23:09 +01:00
b638f7037a Force Slack link unfurling (#763) 2019-03-12 22:56:43 +01:00
74699a8262 Split-out Slack user and channel management (#762) 2019-03-12 22:52:36 +01:00
eabf2a4582 Check module files in CI run (#761) 2019-03-12 22:47:18 +01:00
Wim
325d62b41c Update vendor d5/tengo 2019-03-05 23:10:45 +01:00
Wim
e955a056e2 Trim <p> and </p> tags (matrix). Closes #686 (#753) 2019-03-03 00:29:29 +01:00
Wim
723f8c5fd5 Only build travis on master branch 2019-03-03 00:24:49 +01:00
Wim
a16137f53f Bump version 2019-03-02 23:48:46 +01:00
Wim
d60b8b97f9 Add related projects to README 2019-03-02 23:48:30 +01:00
Wim
7b0bc51183 Release v1.14.0-rc2 2019-03-02 23:23:43 +01:00
Wim
53aa076555 Do not send duplicate messages (rocketchat). Fixes #745 (#752)
For an unknown reason we get duplicate messages (from the same channel)
using the realtime API when we have > 1 channel subscribed on.
Solution for now is caching the message ID in a LRU cache and ignoring
the duplicates.

This should be reviewed when we have actual editing support from the
realtime API
2019-03-02 22:58:14 +01:00
Wim
f57370f33a Add support for URL in messageEntities (telegram). Fixes #735 (#736) 2019-03-02 22:38:44 +01:00
Wim
c557d51b6f Need to specify /topic:mytopic for channel configuration (zulip). (#751)
Breaking change for zulip channel configuration.

For zulip the channel configuration will now need to specify also
the topic with /topic:yourtopic.

Example:
[[gateway.inout]]
account="zulip.streamchat"
channel="general/topic:mytopic"

This fixes the incorrect PR #701 which didn't work with multiple
gateways.
2019-03-02 20:31:38 +01:00
Wim
df3fdc26a0 Use whatsapp forks (#750) 2019-03-02 13:04:28 +01:00
Wim
af00c34aac Do not relay any bot messages (discord) (#743) 2019-02-28 12:59:52 +01:00
Wim
120bf39f55 Handle file upload/download only once for each message (#742) 2019-02-27 20:52:05 +01:00
Wim
26a7e35f27 Add MediaConvertWebPToPNG option (telegram). (#741)
* Add MediaConvertWebPToPNG option (telegram).

When enabled matterbridge will convert .webp files to .png files
before uploading them to the mediaserver of the other bridges.

Fixes #398
2019-02-27 00:41:50 +01:00
Wim
d44d2a5f00 Build on all branches 2019-02-26 20:47:30 +01:00
Wim
7f1d86b338 Fail gracefully on incorrect human input. Fixes #739 (#740) 2019-02-26 18:03:50 +01:00
Wim
d8816280f0 Update changelog 2019-02-26 17:44:35 +01:00
Wim
b09a73040f Print errors as string instead of %#v (#738) 2019-02-26 17:21:23 +01:00
Wim
740b5f2602 Keep reconnecting until succeed (zulip) (#737) 2019-02-26 17:08:20 +01:00
Wim
96841c70c7 Fix regression in HTML handling (telegram). Closes #734
* Revert back to blackfriday v1
* Add testing
2019-02-24 15:13:56 +01:00
Wim
f92735d35d Add goreleaser.yml 2019-02-24 01:09:53 +01:00
Wim
516fd3c92d Release v1.14.0-rc1 2019-02-23 23:20:25 +01:00
Wim
a775b57134 Do not send topic changes on connect (xmpp). Fixes #732 (#733)
This checks if we get a topic change < 5 seconds after connection.
If that's the case, ignore it.
Also this PR makes the topic change an actual EventTopicChange.
2019-02-23 23:03:21 +01:00
Wim
bf21604d42 Make all loggers derive from non-default instance (#728) 2019-02-23 22:51:27 +01:00
Wim
1bb39eba87 Add scripting (tengo) support for every incoming message (#731)
TengoModifyMessage allows you to specify the location of a tengo (https://github.com/d5/tengo/) script.
This script will receive every incoming message and can be used to modify the Username and the Text of that message.
The script will have the following global variables:
to modify: msgUsername and msgText
to read: msgChannel and msgAccount

The script is reloaded on every message, so you can modify the script on the fly.

Example script can be found in https://github.com/42wim/matterbridge/tree/master/gateway/bench.tengo
and https://github.com/42wim/matterbridge/tree/master/contrib/example.tengo

The example below will check if the text contains blah and if so, it'll replace the text and the username of that message.
text := import("text")
if text.re_match("blah",msgText) {
    msgText="replaced by this"
    msgUsername="fakeuser"
}

More information about tengo on: https://github.com/d5/tengo/blob/master/docs/tutorial.md and
https://github.com/d5/tengo/blob/master/docs/stdlib.md
2019-02-23 16:39:44 +01:00
Wim
3190703dc8 Support rewriting messages from relaybots using ExtractNicks. Fixes #466 (#730)
some examples:
this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
you can use multiple entries for multiplebots
this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
2019-02-23 16:35:54 +01:00
Wim
5095db8a43 Update vendor 2019-02-23 14:14:29 +01:00
Wim
1f1634ea59 Add extra debug option (slack) 2019-02-22 19:36:50 +01:00
a7dd033c3b Allow sending discriminator with Discord username (#726) 2019-02-22 14:28:27 +01:00
Wim
95e78ffa05 Add telegram support to matterbridge chat channel 2019-02-21 21:35:34 +01:00
Wim
42276ea7d0 Disable updateChannelMembers for now 2019-02-21 21:26:12 +01:00
Wim
dffd67eb31 Fix typo 2019-02-21 20:33:49 +01:00
55e79063d6 Add initial WhatsApp support (#711) 2019-02-21 20:28:13 +01:00
Wim
46f4bbb3b5 Update documentation wrt ShowJoinPart from discord 2019-02-21 18:15:01 +01:00
Wim
240559581a Handle quit message relay better on gateways with one channel on the irc bridge #722 2019-02-21 17:55:04 +01:00
Wim
48ba829465 Bump to 1.14.0-dev 2019-02-17 22:46:07 +01:00
Wim
eef654de98 Fix bug in #721 2019-02-17 22:45:23 +01:00
Wim
d76a04bd0a Support quits from irc correctly. Fixes #722 (#724) 2019-02-17 22:43:04 +01:00
Wim
a8fe54a78d Allow zulip bridge to specify topic per channel. Closes #701 (#723) 2019-02-17 21:50:05 +01:00
Wim
0bcb0b882f Support join/leaves from discord. Closes #654 (#721) 2019-02-17 21:49:45 +01:00
Wim
4525fa31aa Allow regexs in ignoreNicks. Closes #690 (#720) 2019-02-17 21:49:28 +01:00
Wim
aeaea0574f Detect html nicks in RemoteNickFormat (matrix). Fixes #696 (#719) 2019-02-17 21:48:32 +01:00
Wim
99d71c2177 Send notices on join/parts (matrix). Fixes #712 (#716) 2019-02-16 18:36:09 +01:00
Wim
3e60cfafd3 Send username when uploading video/images (matrix). Fixes #715 (#717) 2019-02-16 18:35:36 +01:00
Wim
3123695869 Upgrade to latest girc version (irc) (#718) 2019-02-16 17:24:04 +01:00
Wim
777af73e2b Add blogpost about matterbridge 2019-02-15 19:02:29 +01:00
Wim
716751cf76 Refactor and update RocketChat bridge (#707)
* Add support for editing/deleting messages
* Add support for uploading files
* Add support for avatars
* Use the Rocket.Chat.Go.SDK
* Use the rest and streaming api
2019-02-15 18:20:32 +01:00
Wim
6ebd5cbbd8 Refactor and update RocketChat bridge
* Add support for editing/deleting messages
* Add support for uploading files
* Add support for avatars
* Use the Rocket.Chat.Go.SDK
* Use the rest and streaming api
2019-02-15 18:19:34 +01:00
Wim
077b818d82 Add extra debug of SubMessage to empty messages error (slack). #709 2019-02-15 18:05:10 +01:00
Wim
5af1d80055 Do not panic on non-json response from server (zulip) 2019-02-13 00:29:34 +01:00
Wim
f236d12166 Update golangci-lint. Disable hugeParam check for now 2019-02-12 20:30:36 +01:00
Wim
127eb908f3 Add fbridge to README 2019-02-12 17:21:27 +01:00
Wim
40d76b2296 Fix error handling on bad event queue id (zulip). Closes #694 2019-02-11 01:34:50 +01:00
Wim
8147815037 Update README. Add rocketchat 2019-02-10 23:32:17 +01:00
Wim
57f156be83 Hint at thread replies when messages are unthreaded (slack) (#684) 2019-02-10 17:23:50 +01:00
2cfd880cdb Clarify dev chat info (#700) 2019-02-05 20:40:57 +01:00
Wim
430b38e770 Update README 2019-01-31 17:38:02 +01:00
Wim
e7f463a082 Bump version 2019-01-31 17:26:07 +01:00
Wim
1d39c771e4 Release v1.13.1 2019-01-31 17:10:57 +01:00
Wim
c81c0dd22a Update vendor, move to labstack/echo/v4 Fixes #698 2019-01-31 17:06:36 +01:00
Wim
f8a1ab4622 Bump version 2019-01-31 00:15:48 +01:00
Wim
5d3309fdcd Release v1.13.0 2019-01-30 23:55:37 +01:00
Wim
4ae028fe73 Optimize handling of very large slack teams. Fixes #695
Stop getting users if we reach 2000 users. Slack will rate-limit us
even if we follow their limits.
This means that we now have to lookup every user that says a message
for the first time. This should be less intensive on the API.

This also disables partly fb713ed91b for now.
ChannelMembers will not be filled.
2019-01-30 23:28:37 +01:00
Wim
707db950c8 Send GetChannelMembers event only to slack for now 2019-01-24 22:46:05 +01:00
Wim
94812d8648 Handle servers without MOTD (irc). Closes #692 2019-01-24 21:58:27 +01:00
Wim
8548b69e6e Fix possible data race (irc). Closes #693 2019-01-24 21:51:52 +01:00
Wim
e3cb665d92 Make discord user token work correctly (discord) #689 2019-01-19 20:39:58 +01:00
Wim
fb713ed91b Add initial support for getting ChannelMember info of all bridges (#678)
* Add initial support for getting ChannelMember info of all bridges.

Adds an EventGetChannelMembers event, which gets send every x time to
all bridges. Bridges should respond on this event with a Message
containing ChannelMembers in the EventGetChannelMembers key in the
Extra field.

handleEventGetChannelMembers will handle this Message and sets the
contained ChannelMembers to the Bridge struct.

* Add ChannelMembers support to the slack bridge
2019-01-18 18:35:31 +01:00
Wim
d99eacc2e1 Run go fmt 2019-01-14 19:41:32 +01:00
62e55214fc Allow to bridge non-bot Discord users (discord) (#689)
If you prefix a token with `User ` it'll treat is as a user token.

Co-Authored-By: zomboy-alfrir <zomboy@dancodes.com.ar>
2019-01-14 19:27:49 +01:00
Wim
464d27ad7e Revert "Update pinned golangci-lint version (#666)"
This reverts commit 015c076315.
Goimports regression: https://github.com/golangci/golangci-lint/issues/347
And gocritic recommending fixes in tip instead of released versions.
2019-01-14 19:17:41 +01:00
f88c5f6c08 Fix displaying usernames for plain text clients. (matrix) (#685) 2019-01-09 23:15:26 +01:00
Wim
da6ce791bc Add link to API page on the wiki 2019-01-09 23:10:32 +01:00
b33b50987b Add support for mattermost threading (#627) 2019-01-09 21:50:03 +01:00
5193634a52 Use only one webhook if possible (discord) (#681) 2019-01-09 21:28:47 +01:00
Wim
0d94746f4a Add api.yaml to contrib 2019-01-09 00:32:04 +01:00
Wim
85680935d4 Add swaggerhub link to README (api) 2019-01-09 00:27:23 +01:00
Wim
46e2683995 Add file comment to webhook messages (discord). Fixes #358 2019-01-07 22:16:00 +01:00
492722af8b Improve error reporting on failure to join Discord. Fixes #672 (#680) 2019-01-07 21:39:53 +01:00
Wim
56749dfb20 Fail if channel starts with hashtag (mattermost). Closes #625 2019-01-07 00:26:11 +01:00
Wim
04567c765e Add support for markdown to HTML conversion (matrix). Closes #663 (#670)
This uses our own gomatrix lib with the SendHTML function which
adds HTML to formatted_body in matrix.
golang-commonmark is used to convert markdown into valid HTML.
2019-01-06 22:25:19 +01:00
048158ad6d Update README about xmpp. Fixes #676 (#677) 2019-01-06 19:34:47 +01:00
7326b9e10d Add various sshchat fixes (#675)
* SSH-Chat: set quiet mode to filter joins/quits
* SSH-Chat: Trim newlines in the end of relayed messages
* SSH-Chat: fix media links
* SSH-Chat: do not relay "Rate limiting is in effect" message
2019-01-05 15:42:36 +01:00
8522d8f29c Fix #668 strip lang in code fences sent to Slack (#673) 2019-01-04 20:32:58 +01:00
Wim
bab385c342 Remove unused key (config) 2019-01-04 16:37:45 +01:00
Wim
bb27ef7939 Add link to matterbridge and k8s article 2019-01-04 16:28:12 +01:00
Wim
d2044c647b Update vendor
* go-telegram-bot-api/telegram-bot-api
* lrstanley/girc
* matterbridge/gomatrix
2019-01-03 00:07:50 +01:00
Wim
c585d00f16 Ignore LatencyReport event (slack) 2019-01-02 23:55:00 +01:00
015c076315 Update pinned golangci-lint version (#666) 2018-12-30 21:29:05 +01:00
Wim
426aa33723 Try building arm docker image 2018-12-26 17:27:25 +01:00
da8e415ae1 Use logrus imports instead of log (#662) 2018-12-26 15:16:09 +01:00
1b834c6858 Fix sshchat connection logic (#661) 2018-12-26 15:09:36 +01:00
d82726cd1b Try downloading files again if slack is too slow (slack). Closes #655 (#656) 2018-12-19 22:01:05 +01:00
Wim
288f0a06bb Bump version 2018-12-15 23:33:13 +01:00
Wim
0121d75032 Update changelog 2018-12-15 23:32:48 +01:00
Wim
53c86702a3 Add wait option for populateUsers/Channels (slack) Fixes #579 (#653)
When setting wait to true, we wait until the populating isn't in progress anymore.
This is used on startup connections where we really need the initial information
which could take a long time on big servers.
2018-12-15 23:11:03 +01:00
192fe89789 Populate user on channel join (slack) (#644) 2018-12-15 22:57:54 +01:00
Wim
959ca3cef3 Fix bot (legacy token) messages not being send. Closes #571 2018-12-13 20:49:14 +01:00
Wim
ccd55d2a28 Refactor gateway (#648)
* Decrease complexity of handleMessage, handleReceive, handleFiles
* Move handlers to handlers.go
* Split ignoreMessage up in ignoreTextEmpty, ignoreNicks and IgnoreTexts
* Add ignoreEvent
* Add testcase for ignoreTextEmpty, ignoreNicks, ignoreTexts and ignoreEvent
2018-12-12 23:57:17 +01:00
Wim
bfa9a83d31 Refactor telegram (#649)
* Decrease complexity in Send() (makes codeclimate happy)
2018-12-12 23:50:08 +01:00
Wim
2f7b4d7f68 Refactor sshchat bridge (#650)
* Decrease complexity in Send()
* Add handleUploadFile() function
2018-12-12 23:47:07 +01:00
Wim
d887855e16 Add bot debug info (slack) 2018-12-12 00:27:55 +01:00
Wim
1a1e68ec98 Enable gocyclo linter 2018-12-09 14:25:17 +01:00
a2754f15fc Enable errcheck linter (#646) 2018-12-08 17:04:10 +01:00
Wim
b6d81f34ba Add repology link 2018-12-07 23:55:08 +01:00
Wim
f9fb33e696 Refactor steam bridge (#630)
* split up in different files
* decrease complexity
2018-12-07 23:48:24 +01:00
Wim
f72d5de2d7 Disable some unparam checks (discord) 2018-12-07 23:48:00 +01:00
0365c0786a Split Discord bridge in multiple files (#632) 2018-12-07 23:36:01 +01:00
af7a00d030 Enable gosec linter (#645) 2018-12-06 00:40:55 +01:00
8a7efce941 Move golangci-lint configuration to file (#635) 2018-12-05 11:34:34 +01:00
ce73aa5a74 Add FOSSRIT/infrastructure, alphabetically sort related project list (#643) 2018-12-04 23:39:31 +01:00
Wim
64d63a25cc Bump version 2018-12-04 10:36:02 +01:00
Wim
2cef9c4fcf Update changelog 2018-12-04 10:35:15 +01:00
Wim
4265d43096 Refactor handleUploadFile (matrix) (#629) 2018-12-03 16:51:11 +01:00
25857591a2 Add note about slack/slack-legacy issues on threading. (#634) 2018-12-03 16:50:37 +01:00
Wim
27f5a1a685 Fix multiple channel join regression. Closes #639 2018-12-03 16:37:12 +01:00
84da2d6a29 Clean-up TravisCI config and add test coverage (#633) 2018-12-03 00:07:15 +01:00
Wim
859ebad55d Make slack-legacy change less restrictive (#626) 2018-12-02 23:09:21 +01:00
e538a4d304 Update nlopes/slack to 4.1-dev (#595) 2018-12-01 19:55:35 +01:00
Wim
f94c2b40a3 Refactor mattermost bridge (#622)
* Split up in different files
* Decrease complexity
* Fix linting issues
2018-12-01 00:49:08 +01:00
47d29ecf63 Tidy up fetching of config values. (#616) 2018-12-01 00:24:07 +01:00
f2088a687e Extract bridgeMap into own package to improve testability (#601) 2018-11-30 23:53:00 +01:00
Wim
faeeee2948 Refactor matterclient (#613)
* Split up in different files
* Decrease complexity
2018-11-29 23:53:43 +01:00
Wim
7923cfe8f8 Fix telegram crash #620 2018-11-29 23:03:50 +01:00
Wim
b51d0a9b05 Bump version 2018-11-29 00:20:09 +01:00
Wim
09f22a801e Release v1.12.1 2018-11-29 00:19:37 +01:00
Wim
3a824c5f9d Update changelog 2018-11-29 00:19:27 +01:00
Wim
df02f51c56 Fix regression on using server ID (discord). #619 #617 2018-11-28 23:50:40 +01:00
fc5e3a6728 Create getChannelsByX functions to make codeclimate happy (slack) (#610) 2018-11-28 11:04:26 +01:00
Wim
57fbd3c723 Refactor irc handlers. Fix linting (#611) 2018-11-28 10:58:56 +01:00
Wim
25cd1e2cc1 Refactor telegram handlers. Fix linting (#609)
* Refactor telegram handlers. Fix linting
2018-11-28 10:57:59 +01:00
f5659d455d Sync channel topics between Slack bridges (#585)
Added logic to allow for configurable synchronisation of topics and purposes of channels between Slack bridges.
2018-11-26 09:47:04 +00:00
Wim
5ed7abdbeb Drop support for mattermost 3.x 2018-11-25 22:38:15 +01:00
09875fe160 Update direct dependencies where possible 2018-11-25 21:21:04 +01:00
Wim
f716b8fc0f Merge pull request #606 from 42wim/fix-590 2018-11-25 20:40:22 +01:00
Wim
9f66f93641 Add option to send RAW commands after connection (irc). Fixes #490 (#604) 2018-11-25 19:32:16 +01:00
Wim
f00d4d7d3f Make sure threaded files stay in thread (slack). Fixes #590 2018-11-25 19:27:45 +01:00
Wim
0929535b2e Do not post empty messages (slack). Fixes #574 2018-11-25 19:26:47 +01:00
Wim
8869e253ca Handle deleted/edited thread starting messages (slack). Fixes #600 (#605) 2018-11-25 10:08:57 +00:00
f3a5ea2956 Remove double " from Discord gateway webhookurl= (#607) 2018-11-25 10:37:14 +01:00
Wim
f4d4dc91b1 Add option to ignore failing bridge on start. Fixes #455 (#603) 2018-11-25 10:35:35 +01:00
1066 changed files with 158262 additions and 28346 deletions

212
.golangci.yaml Normal file
View File

@ -0,0 +1,212 @@
# For full documentation of the configuration options please
# see: https://github.com/golangci/golangci-lint#config-file.
# options for analysis running
run:
# default concurrency is the available CPU number
# concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 1m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# include test files or not, default is true
tests: true
# list of build tags, all linters use it. Default is empty list.
build-tags:
# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs:
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
print-linter-name: true
# all available settings of specific linters, we can set an option for
# a given linter even if we deactivate that same linter at runtime
linters-settings:
errcheck:
# report about not checking of errors in type assertions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
govet:
# report about shadowed variables
check-shadowing: true
golint:
# minimal confidence for issues, default is 0.8
min-confidence: 0.8
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
goimports:
# put imports beginning with prefix after 3rd-party packages;
# it's a comma-separated list of prefixes
local-prefixes: github.com
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 15
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
dupl:
# tokens count to trigger issue, 150 by default
threshold: 150
goconst:
# minimal length of string constant, 3 by default
min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 3
depguard:
list-type: blacklist
include-go-root: false
packages:
# List of packages that we would want to blacklist for... reasons.
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 150
# tab width in spaces. Default to 1.
tab-width: 1
unused:
# treat code as a program (not a library) and report unused exported identifiers; default is false.
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
unparam:
# call graph construction algorithm (cha, rta). In general, use cha for libraries,
# and rta for programs with main packages. Default is cha.
algo: rta
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
nakedret:
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
max-func-lines: 0 # Warn on all naked returns.
prealloc:
# XXX: we don't recommend using this linter before doing performance profiling.
# For most programs usage of prealloc will be a premature optimization.
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# True by default.
simple: true
range-loops: true # Report preallocation suggestions on range loops, true by default
for-loops: false # Report preallocation suggestions on for loops, false by default
gocritic:
# which checks should be enabled; can't be combined with 'disabled-checks';
# default are: [appendAssign assignOp caseOrder dupArg dupBranchBody dupCase flagDeref
# ifElseChain regexpMust singleCaseSwitch sloppyLen switchTrue typeSwitchVar underef
# unlambda unslice rangeValCopy defaultCaseOrder];
# all checks list: https://github.com/go-critic/checkers
# disabled for now - hugeParam
enabled-checks:
- appendAssign
- assignOp
- boolExprSimplify
- builtinShadow
- captLocal
- caseOrder
- commentedOutImport
- defaultCaseOrder
- dupArg
- dupBranchBody
- dupCase
- dupSubExpr
- elseif
- emptyFallthrough
- ifElseChain
- importShadow
- indexAlloc
- methodExprCall
- nestingReduce
- offBy1
- ptrToRefParam
- regexpMust
- singleCaseSwitch
- sloppyLen
- switchTrue
- typeSwitchVar
- typeUnparen
- underef
- unlambda
- unnecessaryBlock
- unslice
- valSwap
- wrapperFunc
- yodaStyleExpr
# linters that we should / shouldn't run
linters:
enable-all: true
disable:
- gochecknoglobals
- lll
- maligned
- prealloc
# rules to deal with reported isues
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: true
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing
# large codebase. It's not practical to fix all existing issues at the moment
# of integration: much better don't allow issues in new code.
# Default is false.
new: false
# Show only new issues created after git revision `REV`
new-from-rev: "HEAD~1"

34
.goreleaser.yml Normal file
View File

@ -0,0 +1,34 @@
release:
prerelease: auto
name_template: "{{.ProjectName}} v{{.Version}}"
builds:
- env:
- CGO_ENABLED=0
goos:
- freebsd
- windows
- darwin
- linux
- dragonfly
- netbsd
- openbsd
goarch:
- amd64
- arm
- arm64
- 386
ldflags:
- -s -w -X main.githash={{.ShortCommit}}
archive:
name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format: binary
files:
- none*
replacements:
386: 32bit
amd64: 64bit
checksum:
name_template: 'checksums.txt'

View File

@ -1,6 +1,7 @@
language: go
go:
- 1.11.x
go_import_path: github.com/42wim/matterbridge
# we have everything vendored
install: true
@ -9,9 +10,10 @@ git:
depth: 200
env:
global:
- GOOS=linux GOARCH=amd64
# - GOOS=windows GOARCH=amd64
#- GOOS=linux GOARCH=arm
- GO111MODULE=on
- GOLANGCI_VERSION="v1.14.0"
matrix:
# It's ok if our code fails on unstable development versions of Go.
@ -25,31 +27,49 @@ notifications:
email: false
before_script:
- MY_VERSION=$(git describe --tags)
# - GO_FILES=$(find . -iname '*.go' | grep -v /vendor/) # All the .go files, excluding vendor/
- PKGS=$(go list ./... | grep -v /vendor/) # All the import paths, excluding vendor/
# - go get github.com/golang/lint/golint # Linter
#- go get honnef.co/go/tools/cmd/megacheck # Badass static analyzer/linter
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.12.2
# Get version info from tags.
- MY_VERSION="$(git describe --tags)"
# Retrieve the golangci-lint linter binary.
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION}
# Retrieve and prepare CodeClimate's test coverage reporter.
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
# Anything in before_script: that returns a nonzero exit code will
# flunk the build and immediately stop. It's sorta like having
# set -e enabled in bash.
script:
#- test -z "$(go fmt ./...)" # 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 vet $PKGS # go vet is the official Go static analyzer
- golangci-lint run --enable-all -D lll -D errcheck -D gosec -D maligned -D prealloc -D gocyclo -D gochecknoglobals
#- megacheck $PKGS # "go vet on steroids" + linter
# Ensure that the module files are being kept correctly and that vendored dependencies are up-to-date.
- go mod tidy
- go mod vendor
- git diff --exit-code --quiet || (echo "Please run 'go mod tidy' to clean up the 'go.mod' and 'go.sum' files."; false)
# Run the linter.
- golangci-lint run
# Run all the tests with the race detector and generate coverage.
- go test -v -race -coverprofile c.out ./...
# Run the build script to generate the necessary binaries and images.
- /bin/bash ci/bintray.sh
#- golint -set_exit_status $PKGS # one last linter
after_script:
# Upload test coverage to CodeClimate.
- ./cc-test-reporter after-build --exit-code ${TRAVIS_TEST_RESULT}
branches:
only:
- master
deploy:
on:
all_branches: true
provider: bintray
on:
all_branches: true
edge:
branch: v1.8.47
file: ci/deploy.json
user: 42wim
on:
all_branches: true
key:
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="

View File

@ -5,7 +5,7 @@
![Matterbridge Logo](img/matterbridge-notext.gif)<br />
**A simple chat bridge**<br />
Letting people be where they want to be.<br />
<sub>Bridges between a growing number of protocols. Click below to demo.</sub>
<sub>Bridges between a growing number of protocols. Click below to demo or join the development chat.</sub>
<sup>
@ -15,9 +15,12 @@
[Matrix][mb-matrix] |
[Slack][mb-slack] |
[Mattermost][mb-mattermost] |
[Rocket.Chat][mb-rocketchat] |
[XMPP][mb-xmpp] |
[Twitch][mb-twitch] |
[WhatsApp][mb-whatsapp] |
[Zulip][mb-zulip] |
[Telegram][mb-telegram] |
And more...
</sup>
@ -34,8 +37,10 @@
### Table of Contents
* [Features](https://github.com/42wim/matterbridge/wiki/Features)
* [Natively supported](#natively-supported)
* [3rd party via matterbridge api](#3rd-party-via-matterbridge-api)
* [API](#API)
* [Requirements](#requirements)
* [Chat with us](#chat-with-us)
* [Screenshots](https://github.com/42wim/matterbridge/wiki/)
* [Installing](#installing)
* [Binaries](#binaries)
@ -61,20 +66,11 @@
* [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)
* [API](https://github.com/42wim/matterbridge/wiki/Features#api)
### API
The API is very basic at the moment and rather undocumented.
### Natively supported
Used by at least 3 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)
* [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
## Requirements
Accounts to one of the supported bridges
* [Mattermost](https://github.com/mattermost/platform/) 3.8.x - 3.10.x, 4.x, 5.x
* [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x
* [IRC](http://www.mirc.com/servers.html)
* [XMPP](https://jabber.org)
* [XMPP](https://xmpp.org)
* [Gitter](https://gitter.im)
* [Slack](https://slack.com)
* [Discord](https://discordapp.com)
@ -85,18 +81,56 @@ Accounts to one of the supported bridges
* [Steam](https://store.steampowered.com/)
* [Twitch](https://twitch.tv)
* [Ssh-chat](https://github.com/shazow/ssh-chat)
* [WhatsApp](https://www.whatsapp.com/)
* [Zulip](https://zulipchat.com)
### 3rd party via matterbridge api
* [Minecraft](https://github.com/elytra/MatterLink)
* [Reddit](https://github.com/bonehurtingjuice/mattereddit)
* [Facebook messenger](https://github.com/VictorNine/fbridge)
* [Discourse](https://github.com/DeclanHoare/matterbabble)
### API
The API is very basic at the moment.
More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api).
Used by the projects below. 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)
* [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
* [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
## Chat with us
Questions or want to test on your favorite platform? Join below:
* [Gitter][mb-gitter]
* [IRC][mb-irc]
* [Discord][mb-discord]
* [Matrix][mb-matrix]
* [Slack][mb-slack]
* [Mattermost][mb-mattermost]
* [Rocket.Chat][mb-rocketchat]
* [XMPP][mb-xmpp]
* [Twitch][mb-twitch]
* [Zulip][mb-zulip]
* [Telegram][mb-telegram]
## Screenshots
See https://github.com/42wim/matterbridge/wiki
## Installing
### Binaries
* Latest stable release [v1.12.0](https://github.com/42wim/matterbridge/releases/latest)
* Latest stable release [v1.14.1](https://github.com/42wim/matterbridge/releases/latest)
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
### Packages
* [Overview](https://repology.org/metapackage/matterbridge/versions)
### Building
Go 1.8+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH](https://golang.org/doc/code.html#GOPATH).
Go 1.9+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH](https://golang.org/doc/code.html#GOPATH).
After Go is setup, download matterbridge to your $GOPATH directory.
@ -205,20 +239,21 @@ See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.m
See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
Want to tip ?
* eth: 0xb3f9b5387c66ad6be892bcb7bbc67862f3abc16f
* btc: 1N7cKHj5SfqBHBzDJ6kad4BzeqUBBS2zhs
## Related projects
* [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
* [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
* [FOSSRIT/infrastructure - roles/matterbridge](https://github.com/FOSSRIT/infrastructure/tree/master/roles/matterbridge) (Ansible role used to automate deployments of Matterbridge)
* [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig)
* [matterlink](https://github.com/elytra/MatterLink)
* [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
* [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
* [mattereddit](https://github.com/bonehurtingjuice/mattereddit)
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
* [matterlink](https://github.com/elytra/MatterLink)
* [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
* [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
* [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
## Articles
* [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
* https://mattermost.com/blog/connect-irc-to-mattermost/
* https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/
* https://blog.brightscout.com/top-10-mattermost-integrations/
@ -226,6 +261,7 @@ Want to tip ?
* https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/
* https://kopano.com/blog/matterbridge-bridging-mattermost-chat/
* https://www.stitcher.com/s/?eid=52382713
* https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/
## Thanks
[![Digitalocean](https://snag.gy/3LVifX.jpg)](https://www.digitalocean.com/) for sponsoring demo/testing droplets.
@ -237,13 +273,16 @@ Matterbridge wouldn't exist without these libraries:
* gops - https://github.com/google/gops
* gozulipbot - https://github.com/ifo/gozulipbot
* irc - https://github.com/lrstanley/girc
* mattermost - https://github.com/mattermost/platform
* mattermost - https://github.com/mattermost/mattermost-server
* matrix - https://github.com/matrix-org/gomatrix
* sshchat - https://github.com/shazow/ssh-chat
* slack - https://github.com/nlopes/slack
* steam - https://github.com/Philipp15b/go-steam
* telegram - https://github.com/go-telegram-bot-api/telegram-bot-api
* xmpp - https://github.com/mattn/go-xmpp
* whatsapp - https://github.com/Rhymen/go-whatsapp/
* zulip - https://github.com/ifo/gozulipbot
* tengo - https://github.com/d5/tengo
<!-- Links -->
@ -253,6 +292,9 @@ Matterbridge wouldn't exist without these libraries:
[mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org
[mb-slack]: https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA
[mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e
[mb-rocketchat]: https://open.rocket.chat/channel/matterbridge
[mb-xmpp]: https://inverse.chat/
[mb-twitch]: https://www.twitch.tv/matterbridge
[mb-whatsapp]: https://www.whatsapp.com/
[mb-zulip]: https://matterbridge.zulipchat.com/register/
[mb-telegram]: https://t.me/Matterbridge

View File

@ -8,8 +8,8 @@ import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/zfjagann/golang-ring"
)
@ -117,20 +117,14 @@ func (b *API) handleStream(c echo.Context) error {
return err
}
c.Response().Flush()
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()
msg := b.Messages.Dequeue()
if msg != nil {
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
return err
}
time.Sleep(200 * time.Millisecond)
c.Response().Flush()
}
time.Sleep(200 * time.Millisecond)
}
}

View File

@ -1,10 +1,11 @@
package bridge
import (
"github.com/42wim/matterbridge/bridge/config"
log "github.com/sirupsen/logrus"
"strings"
"sync"
"github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus"
)
type Bridger interface {
@ -16,42 +17,52 @@ type Bridger interface {
type Bridge struct {
Bridger
Name string
Account string
Protocol string
Channels map[string]config.ChannelInfo
Joined map[string]bool
Log *log.Entry
Config config.Config
General *config.Protocol
*sync.RWMutex
Name string
Account string
Protocol string
Channels map[string]config.ChannelInfo
Joined map[string]bool
ChannelMembers *config.ChannelMembers
Log *logrus.Entry
Config config.Config
General *config.Protocol
}
type Config struct {
// General *config.Protocol
Remote chan config.Message
Log *log.Entry
*Bridge
Remote chan config.Message
}
// Factory is the factory function to create a bridge
type Factory func(*Config) Bridger
func New(bridge *config.Bridge) *Bridge {
b := new(Bridge)
b.Channels = make(map[string]config.ChannelInfo)
accInfo := strings.Split(bridge.Account, ".")
protocol := accInfo[0]
name := accInfo[1]
b.Name = name
b.Protocol = protocol
b.Account = bridge.Account
b.Joined = make(map[string]bool)
return b
return &Bridge{
RWMutex: new(sync.RWMutex),
Channels: make(map[string]config.ChannelInfo),
Name: name,
Protocol: protocol,
Account: bridge.Account,
Joined: make(map[string]bool),
}
}
func (b *Bridge) JoinChannels() error {
err := b.joinChannels(b.Channels, b.Joined)
return err
return b.joinChannels(b.Channels, b.Joined)
}
// SetChannelMembers sets the newMembers to the bridge ChannelMembers
func (b *Bridge) SetChannelMembers(newMembers *config.ChannelMembers) {
b.Lock()
b.ChannelMembers = newMembers
b.Unlock()
}
func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {

View File

@ -8,22 +8,22 @@ import (
"time"
"github.com/fsnotify/fsnotify"
prefixed "github.com/matterbridge/logrus-prefixed-formatter"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
const (
EventJoinLeave = "join_leave"
EventTopicChange = "topic_change"
EventFailure = "failure"
EventFileFailureSize = "file_failure_size"
EventAvatarDownload = "avatar_download"
EventRejoinChannels = "rejoin_channels"
EventUserAction = "user_action"
EventMsgDelete = "msg_delete"
EventAPIConnected = "api_connected"
EventUserTyping = "user_typing"
EventJoinLeave = "join_leave"
EventTopicChange = "topic_change"
EventFailure = "failure"
EventFileFailureSize = "file_failure_size"
EventAvatarDownload = "avatar_download"
EventRejoinChannels = "rejoin_channels"
EventUserAction = "user_action"
EventMsgDelete = "msg_delete"
EventAPIConnected = "api_connected"
EventUserTyping = "user_typing"
EventGetChannelMembers = "get_channel_members"
)
type Message struct {
@ -61,6 +61,16 @@ type ChannelInfo struct {
Options ChannelOptions
}
type ChannelMember struct {
Username string
Nick string
UserID string
ChannelID string
ChannelName string
}
type ChannelMembers []ChannelMember
type Protocol struct {
AuthCode string // steam
BindAddress string // mattermost, slack // DEPRECATED
@ -72,6 +82,7 @@ type Protocol struct {
EditSuffix string // mattermost, slack, discord, telegram, gitter
EditDisable bool // mattermost, slack, discord, telegram, gitter
IconURL string // mattermost, slack
IgnoreFailureOnStart bool // general
IgnoreNicks string // all protocols
IgnoreMessages string // all protocols
Jid string // xmpp
@ -82,6 +93,7 @@ type Protocol struct {
MediaDownloadSize int // all protocols
MediaServerDownload string
MediaServerUpload string
MediaConvertWebPToPNG bool // telegram
MessageDelay int // IRC, time in millisecond to wait between messages
MessageFormat string // telegram
MessageLength int // IRC, max length of a message allowed
@ -108,6 +120,7 @@ type Protocol struct {
ReplaceMessages [][]string // all protocols
ReplaceNicks [][]string // all protocols
RemoteNickFormat string // all protocols
RunCommands []string // irc
Server string // IRC,mattermost,XMPP,discord
ShowJoinPart bool // all protocols
ShowTopicChange bool // slack
@ -115,6 +128,8 @@ type Protocol struct {
ShowEmbeds bool // discord
SkipTLSVerify bool // IRC, mattermost
StripNick bool // all protocols
SyncTopic bool // slack
TengoModifyMessage string // general
Team string // mattermost
Token string // gitter, slack, discord, api
Topic string // zulip
@ -122,17 +137,18 @@ type Protocol struct {
UseAPI bool // mattermost, slack
UseSASL bool // IRC
UseTLS bool // IRC
UseDiscriminator bool // discord
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 {
Key string // irc, xmpp
WebhookURL string // discord
Topic string // zulip
}
type Bridge struct {
@ -171,6 +187,7 @@ type BridgeValues struct {
Telegram map[string]Protocol
Rocketchat map[string]Protocol
SSHChat map[string]Protocol
WhatsApp map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results
Zulip map[string]Protocol
General Protocol
Gateway []Gateway
@ -187,63 +204,58 @@ type Config interface {
}
type config struct {
v *viper.Viper
sync.RWMutex
cv *BridgeValues
logger *logrus.Entry
v *viper.Viper
cv *BridgeValues
}
func NewConfig(cfgfile string) Config {
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false})
flog := log.WithFields(log.Fields{"prefix": "config"})
// NewConfig instantiates a new configuration based on the specified configuration file path.
func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
viper.SetConfigFile(cfgfile)
input, err := getFileContents(cfgfile)
input, err := ioutil.ReadFile(cfgfile)
if err != nil {
log.Fatal(err)
logger.Fatalf("Failed to read configuration file: %#v", err)
}
mycfg := newConfigFromString(input)
mycfg := newConfigFromString(logger, input)
if mycfg.cv.General.MediaDownloadSize == 0 {
mycfg.cv.General.MediaDownloadSize = 1000000
}
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
flog.Println("Config file changed:", e.Name)
logger.Println("Config file changed:", e.Name)
})
return mycfg
}
func getFileContents(filename string) ([]byte, error) {
input, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
return []byte(nil), err
}
return input, nil
// NewConfigFromString instantiates a new configuration based on the specified string.
func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config {
logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
return newConfigFromString(logger, input)
}
func NewConfigFromString(input []byte) Config {
return newConfigFromString(input)
}
func newConfigFromString(input []byte) *config {
func newConfigFromString(logger *logrus.Entry, input []byte) *config {
viper.SetConfigType("toml")
viper.SetEnvPrefix("matterbridge")
viper.AddConfigPath(".")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()
err := viper.ReadConfig(bytes.NewBuffer(input))
if err != nil {
log.Fatal(err)
if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil {
logger.Fatalf("Failed to parse the configuration: %#v", err)
}
cfg := &BridgeValues{}
err = viper.Unmarshal(cfg)
if err != nil {
log.Fatal(err)
if err := viper.Unmarshal(cfg); err != nil {
logger.Fatalf("Failed to load the configuration: %#v", err)
}
return &config{
v: viper.GetViper(),
cv: cfg,
logger: logger,
v: viper.GetViper(),
cv: cfg,
}
}
@ -254,46 +266,44 @@ func (c *config) BridgeValues() *BridgeValues {
func (c *config) GetBool(key string) (bool, bool) {
c.RLock()
defer c.RUnlock()
// log.Debugf("getting bool %s = %#v", key, c.v.GetBool(key))
return c.v.GetBool(key), c.v.IsSet(key)
}
func (c *config) GetInt(key string) (int, bool) {
c.RLock()
defer c.RUnlock()
// log.Debugf("getting int %s = %d", key, c.v.GetInt(key))
return c.v.GetInt(key), c.v.IsSet(key)
}
func (c *config) GetString(key string) (string, bool) {
c.RLock()
defer c.RUnlock()
// log.Debugf("getting String %s = %s", key, c.v.GetString(key))
return c.v.GetString(key), c.v.IsSet(key)
}
func (c *config) GetStringSlice(key string) ([]string, bool) {
c.RLock()
defer c.RUnlock()
// log.Debugf("getting StringSlice %s = %#v", key, c.v.GetStringSlice(key))
return c.v.GetStringSlice(key), c.v.IsSet(key)
}
func (c *config) GetStringSlice2D(key string) ([][]string, bool) {
c.RLock()
defer c.RUnlock()
result := [][]string{}
if res, ok := c.v.Get(key).([]interface{}); ok {
for _, entry := range res {
result2 := []string{}
for _, entry2 := range entry.([]interface{}) {
result2 = append(result2, entry2.(string))
}
result = append(result, result2)
}
return result, true
res, ok := c.v.Get(key).([]interface{})
if !ok {
return nil, false
}
return result, false
var result [][]string
for _, entry := range res {
result2 := []string{}
for _, entry2 := range entry.([]interface{}) {
result2 = append(result2, entry2.(string))
}
result = append(result, result2)
}
return result, true
}
func GetIconURL(msg *Message, iconURL string) string {

View File

@ -4,7 +4,6 @@ import (
"bytes"
"errors"
"fmt"
"regexp"
"strings"
"sync"
@ -17,18 +16,24 @@ import (
const MessageLength = 1950
type Bdiscord struct {
c *discordgo.Session
Channels []*discordgo.Channel
Nick string
UseChannelID bool
userMemberMap map[string]*discordgo.Member
nickMemberMap map[string]*discordgo.Member
guildID string
webhookID string
webhookToken string
channelInfoMap map[string]*config.ChannelInfo
sync.RWMutex
*bridge.Config
c *discordgo.Session
nick string
useChannelID bool
guildID string
webhookID string
webhookToken string
canEditWebhooks bool
channelsMutex sync.RWMutex
channels []*discordgo.Channel
channelInfoMap map[string]*config.ChannelInfo
membersMutex sync.RWMutex
userMemberMap map[string]*discordgo.Member
nickMemberMap map[string]*discordgo.Member
}
func New(cfg *bridge.Config) bridge.Bridger {
@ -45,7 +50,8 @@ func New(cfg *bridge.Config) bridge.Bridger {
func (b *Bdiscord) Connect() error {
var err error
var token string
var guildFound bool
token := b.GetString("Token")
b.Log.Info("Connecting")
if b.GetString("WebhookURL") == "" {
b.Log.Info("Connecting using token")
@ -55,6 +61,11 @@ func (b *Bdiscord) Connect() error {
if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
token = "Bot " + b.GetString("Token")
}
// if we have a User token, remove the `Bot` prefix
if strings.HasPrefix(b.GetString("Token"), "User ") {
token = strings.Replace(b.GetString("Token"), "User ", "", -1)
}
b.c, err = discordgo.New(token)
if err != nil {
return err
@ -64,6 +75,8 @@ func (b *Bdiscord) Connect() error {
b.c.AddHandler(b.memberUpdate)
b.c.AddHandler(b.messageUpdate)
b.c.AddHandler(b.messageDelete)
b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove)
err = b.c.Open()
if err != nil {
return err
@ -76,28 +89,71 @@ func (b *Bdiscord) Connect() error {
if err != nil {
return err
}
b.Nick = userinfo.Username
serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
b.nick = userinfo.Username
b.channelsMutex.Lock()
for _, guild := range guilds {
if guild.Name == b.GetString("Server") {
b.Channels, err = b.c.GuildChannels(guild.ID)
if guild.Name == serverName || guild.ID == serverName {
b.channels, err = b.c.GuildChannels(guild.ID)
b.guildID = guild.ID
guildFound = true
if err != nil {
return err
break
}
}
}
for _, channel := range b.Channels {
b.Log.Debugf("found channel %#v", channel)
b.channelsMutex.Unlock()
if !guildFound {
msg := fmt.Sprintf("Server \"%s\" not found", b.GetString("Server"))
err = errors.New(msg)
b.Log.Error(msg)
b.Log.Info("Possible values:")
for _, guild := range guilds {
b.Log.Infof("Server=\"%s\" # Server name", guild.Name)
b.Log.Infof("Server=\"%s\" # Server ID", guild.ID)
}
}
// obtaining guild members and initializing nickname mapping
b.Lock()
defer b.Unlock()
if err != nil {
return err
}
b.channelsMutex.RLock()
if b.GetString("WebhookURL") == "" {
for _, channel := range b.channels {
b.Log.Debugf("found channel %#v", channel)
}
} else {
b.canEditWebhooks = true
for _, channel := range b.channels {
b.Log.Debugf("found channel %#v; verifying PermissionManageWebhooks", channel)
perms, permsErr := b.c.State.UserChannelPermissions(userinfo.ID, channel.ID)
manageWebhooks := discordgo.PermissionManageWebhooks
if permsErr != nil || perms&manageWebhooks != manageWebhooks {
b.Log.Warnf("Can't manage webhooks in channel \"%s\"", channel.Name)
b.canEditWebhooks = false
}
}
if b.canEditWebhooks {
b.Log.Info("Can manage webhooks; will edit channel for global webhook on send")
} else {
b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send")
}
}
b.channelsMutex.RUnlock()
// Obtaining guild members and initializing nickname mapping.
b.membersMutex.Lock()
defer b.membersMutex.Unlock()
members, err := b.c.GuildMembers(b.guildID, "", 1000)
if err != nil {
b.Log.Error("Error obtaining guild members", err)
b.Log.Error("Error obtaining server members: ", err)
return err
}
for _, member := range members {
if member == nil {
b.Log.Warnf("Skipping missing information for a user.")
continue
}
b.userMemberMap[member.User.ID] = member
b.nickMemberMap[member.User.Username] = member
if member.Nick != "" {
@ -112,10 +168,13 @@ func (b *Bdiscord) Disconnect() error {
}
func (b *Bdiscord) JoinChannel(channel config.ChannelInfo) error {
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelInfoMap[channel.ID] = &channel
idcheck := strings.Split(channel.Name, "ID:")
if len(idcheck) > 1 {
b.UseChannelID = true
b.useChannelID = true
}
return nil
}
@ -133,16 +192,20 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
msg.Text = "_" + msg.Text + "_"
}
// use initial webhook
// use initial webhook configured for the entire Discord account
isGlobalWebhook := true
wID := b.webhookID
wToken := b.webhookToken
// check if have a channel specific webhook
b.channelsMutex.RLock()
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
if ci.Options.WebhookURL != "" {
wID, wToken = b.splitURL(ci.Options.WebhookURL)
isGlobalWebhook = false
}
}
b.channelsMutex.RUnlock()
// Use webhook to send the message
if wID != "" {
@ -153,8 +216,14 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
b.Log.Debugf("Broadcasting using Webhook")
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text += " " + fi.URL
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
}
// skip empty messages
@ -168,6 +237,19 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
if len(msg.Username) > 32 {
msg.Username = msg.Username[0:32]
}
// if we have a global webhook for this Discord account, and permission
// to modify webhooks (previously verified), then set its channel to
// the message channel before using it
// TODO: this isn't necessary if the last message from this webhook was
// sent to the current channel
if isGlobalWebhook && b.canEditWebhooks {
b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel)
_, err := b.c.WebhookEdit(wID, "", "", channelID)
if err != nil {
b.Log.Errorf("Could not set webhook channel: %s", err)
return "", err
}
}
err := b.c.WebhookExecute(
wID,
wToken,
@ -195,7 +277,9 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength)
b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text)
if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
}
}
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
@ -220,246 +304,15 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
return res.ID, err
}
func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) {
rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
rmsg.Channel = b.getChannelName(m.ChannelID)
if b.UseChannelID {
rmsg.Channel = "ID:" + m.ChannelID
}
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
if b.GetBool("EditDisable") {
return
}
// only when message is actually edited
if m.Message.EditedTimestamp != "" {
b.Log.Debugf("Sending edit message")
m.Content += b.GetString("EditSuffix")
b.messageCreate(s, (*discordgo.MessageCreate)(m))
}
}
func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
var err error
// not relay our own messages
if m.Author.Username == b.Nick {
return
}
// if using webhooks, do not relay if it's ours
if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) {
return
}
// add the url of the attachments to content
if len(m.Attachments) > 0 {
for _, attach := range m.Attachments {
m.Content = m.Content + "\n" + attach.URL
}
}
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
if m.Content != "" {
b.Log.Debugf("== Receiving event %#v", m.Message)
m.Message.Content = b.stripCustomoji(m.Message.Content)
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
if err != nil {
b.Log.Errorf("ContentWithMoreMentionsReplaced failed: %s", err)
rmsg.Text = m.ContentWithMentionsReplaced()
}
}
// set channel name
rmsg.Channel = b.getChannelName(m.ChannelID)
if b.UseChannelID {
rmsg.Channel = "ID:" + m.ChannelID
}
// set username
if !b.GetBool("UseUserName") {
rmsg.Username = b.getNick(m.Author)
} else {
rmsg.Username = m.Author.Username
}
// if we have embedded content add it to text
if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil {
for _, embed := range m.Message.Embeds {
rmsg.Text = rmsg.Text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n"
}
}
// no empty messages
if rmsg.Text == "" {
return
}
// do we have a /me action
var ok bool
rmsg.Text, ok = b.replaceAction(rmsg.Text)
if ok {
rmsg.Event = config.EventUserAction
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
b.Lock()
if _, ok := b.userMemberMap[m.Member.User.ID]; ok {
b.Log.Debugf("%s: memberupdate: user %s (nick %s) changes nick to %s", b.Account, m.Member.User.Username, b.userMemberMap[m.Member.User.ID].Nick, m.Member.Nick)
}
b.userMemberMap[m.Member.User.ID] = m.Member
b.nickMemberMap[m.Member.User.Username] = m.Member
if m.Member.Nick != "" {
b.nickMemberMap[m.Member.Nick] = m.Member
}
b.Unlock()
}
func (b *Bdiscord) getNick(user *discordgo.User) string {
var err error
b.Lock()
defer b.Unlock()
if _, ok := b.userMemberMap[user.ID]; ok {
if b.userMemberMap[user.ID] != nil {
if b.userMemberMap[user.ID].Nick != "" {
// only return if nick is set
return b.userMemberMap[user.ID].Nick
}
// otherwise return username
return user.Username
}
}
// if we didn't find nick, search for it
member, err := b.c.GuildMember(b.guildID, user.ID)
if err != nil {
return user.Username
}
b.userMemberMap[user.ID] = member
// only return if nick is set
if b.userMemberMap[user.ID].Nick != "" {
return b.userMemberMap[user.ID].Nick
}
return user.Username
}
func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error) {
b.Lock()
defer b.Unlock()
if _, ok := b.nickMemberMap[nick]; ok {
if b.nickMemberMap[nick] != nil {
return b.nickMemberMap[nick], nil
}
}
return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
}
func (b *Bdiscord) getChannelID(name string) string {
idcheck := strings.Split(name, "ID:")
if len(idcheck) > 1 {
return idcheck[1]
}
for _, channel := range b.Channels {
if channel.Name == name {
return channel.ID
}
}
return ""
}
func (b *Bdiscord) getChannelName(id string) string {
for _, channel := range b.Channels {
if channel.ID == id {
return channel.Name
}
}
return ""
}
func (b *Bdiscord) replaceChannelMentions(text string) string {
var err error
re := regexp.MustCompile("<#[0-9]+>")
text = re.ReplaceAllStringFunc(text, func(m string) string {
channel := b.getChannelName(m[2 : len(m)-1])
// if at first don't succeed, try again
if channel == "" {
b.Channels, err = b.c.GuildChannels(b.guildID)
if err != nil {
return "#unknownchannel"
}
channel = b.getChannelName(m[2 : len(m)-1])
return "#" + channel
}
return "#" + channel
})
return text
}
func (b *Bdiscord) replaceUserMentions(text string) string {
re := regexp.MustCompile("@[^@]{1,32}")
text = re.ReplaceAllStringFunc(text, func(m string) string {
mention := strings.TrimSpace(m[1:])
var member *discordgo.Member
var err error
for {
b.Log.Debugf("Testing mention: '%s'", mention)
member, err = b.getGuildMemberByNick(mention)
if err != nil {
lastSpace := strings.LastIndex(mention, " ")
if lastSpace == -1 {
break
}
mention = strings.TrimSpace(mention[0:lastSpace])
} else {
break
}
}
if err != nil {
return m
}
return strings.Replace(m, "@"+mention, member.User.Mention(), -1)
})
b.Log.Debugf("Message with mention replaced: %s", text)
return text
}
func (b *Bdiscord) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") {
return strings.Replace(text, "_", "", -1), true
}
return text, false
}
func (b *Bdiscord) stripCustomoji(text string) string {
// <:doge:302803592035958784>
re := regexp.MustCompile("<(:.*?:)[0-9]+>")
return re.ReplaceAllString(text, `$1`)
}
// splitURL splits a webhookURL and returns the id and token
func (b *Bdiscord) splitURL(url string) (string, string) {
webhookURLSplit := strings.Split(url, "/")
if len(webhookURLSplit) != 7 {
b.Log.Fatalf("%s is no correct discord WebhookURL", url)
}
return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1]
}
// useWebhook returns true if we have a webhook defined somewhere
func (b *Bdiscord) useWebhook() bool {
if b.GetString("WebhookURL") != "" {
return true
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
return true
@ -476,6 +329,10 @@ func (b *Bdiscord) isWebhookID(id string) bool {
return true
}
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
wID, _ := b.splitURL(channel.Options.WebhookURL)
@ -503,7 +360,7 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
}
_, err = b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil {
return "", fmt.Errorf("file upload failed: %#v", err)
return "", fmt.Errorf("file upload failed: %s", err)
}
}
return "", nil

170
bridge/discord/handlers.go Normal file
View File

@ -0,0 +1,170 @@
package bdiscord
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/bwmarrin/discordgo"
)
func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam
rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
rmsg.Channel = b.getChannelName(m.ChannelID)
if b.useChannelID {
rmsg.Channel = "ID:" + m.ChannelID
}
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam
if b.GetBool("EditDisable") {
return
}
// only when message is actually edited
if m.Message.EditedTimestamp != "" {
b.Log.Debugf("Sending edit message")
m.Content += b.GetString("EditSuffix")
b.messageCreate(s, (*discordgo.MessageCreate)(m))
}
}
func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam
var err error
// not relay our own messages
if m.Author.Username == b.nick {
return
}
// if using webhooks, do not relay if it's ours
if b.useWebhook() && m.Author.Bot { // && b.isWebhookID(m.Author.ID) {
return
}
// add the url of the attachments to content
if len(m.Attachments) > 0 {
for _, attach := range m.Attachments {
m.Content = m.Content + "\n" + attach.URL
}
}
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
if m.Content != "" {
b.Log.Debugf("== Receiving event %#v", m.Message)
m.Message.Content = b.stripCustomoji(m.Message.Content)
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
if err != nil {
b.Log.Errorf("ContentWithMoreMentionsReplaced failed: %s", err)
rmsg.Text = m.ContentWithMentionsReplaced()
}
}
// set channel name
rmsg.Channel = b.getChannelName(m.ChannelID)
if b.useChannelID {
rmsg.Channel = "ID:" + m.ChannelID
}
// set username
if !b.GetBool("UseUserName") {
rmsg.Username = b.getNick(m.Author)
} else {
rmsg.Username = m.Author.Username
if b.GetBool("UseDiscriminator") {
rmsg.Username += "#" + m.Author.Discriminator
}
}
// if we have embedded content add it to text
if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil {
for _, embed := range m.Message.Embeds {
rmsg.Text = rmsg.Text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n"
}
}
// no empty messages
if rmsg.Text == "" {
return
}
// do we have a /me action
var ok bool
rmsg.Text, ok = b.replaceAction(rmsg.Text)
if ok {
rmsg.Event = config.EventUserAction
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m)
}
b.membersMutex.Lock()
defer b.membersMutex.Unlock()
if currMember, ok := b.userMemberMap[m.Member.User.ID]; ok {
b.Log.Debugf(
"%s: memberupdate: user %s (nick %s) changes nick to %s",
b.Account,
m.Member.User.Username,
b.userMemberMap[m.Member.User.ID].Nick,
m.Member.Nick,
)
delete(b.nickMemberMap, currMember.User.Username)
delete(b.nickMemberMap, currMember.Nick)
delete(b.userMemberMap, m.Member.User.ID)
}
b.userMemberMap[m.Member.User.ID] = m.Member
b.nickMemberMap[m.Member.User.Username] = m.Member
if m.Member.Nick != "" {
b.nickMemberMap[m.Member.Nick] = m.Member
}
}
func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m)
return
}
username := m.Member.User.Username
if m.Member.Nick != "" {
username = m.Member.Nick
}
rmsg := config.Message{
Account: b.Account,
Event: config.EventJoinLeave,
Username: "system",
Text: username + " joins",
}
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m)
return
}
username := m.Member.User.Username
if m.Member.Nick != "" {
username = m.Member.Nick
}
rmsg := config.Message{
Account: b.Account,
Event: config.EventJoinLeave,
Username: "system",
Text: username + " leaves",
}
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}

189
bridge/discord/helpers.go Normal file
View File

@ -0,0 +1,189 @@
package bdiscord
import (
"errors"
"regexp"
"strings"
"unicode"
"github.com/bwmarrin/discordgo"
)
func (b *Bdiscord) getNick(user *discordgo.User) string {
b.membersMutex.RLock()
defer b.membersMutex.RUnlock()
if member, ok := b.userMemberMap[user.ID]; ok {
if member.Nick != "" {
// Only return if nick is set.
return member.Nick
}
// Otherwise return username.
return user.Username
}
// If we didn't find nick, search for it.
member, err := b.c.GuildMember(b.guildID, user.ID)
if err != nil {
b.Log.Warnf("Failed to fetch information for member %#v: %s", user, err)
return user.Username
} else if member == nil {
b.Log.Warnf("Got no information for member %#v", user)
return user.Username
}
b.userMemberMap[user.ID] = member
b.nickMemberMap[member.User.Username] = member
if member.Nick != "" {
b.nickMemberMap[member.Nick] = member
return member.Nick
}
return user.Username
}
func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error) {
b.membersMutex.RLock()
defer b.membersMutex.RUnlock()
if member, ok := b.nickMemberMap[nick]; ok {
return member, nil
}
return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
}
func (b *Bdiscord) getChannelID(name string) string {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
idcheck := strings.Split(name, "ID:")
if len(idcheck) > 1 {
return idcheck[1]
}
for _, channel := range b.channels {
if channel.Name == name {
return channel.ID
}
}
return ""
}
func (b *Bdiscord) getChannelName(id string) string {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channels {
if channel.ID == id {
return channel.Name
}
}
return ""
}
var (
// See https://discordapp.com/developers/docs/reference#message-formatting.
channelMentionRE = regexp.MustCompile("<#[0-9]+>")
emojiRE = regexp.MustCompile("<(:.*?:)[0-9]+>")
userMentionRE = regexp.MustCompile("@[^@\n]{1,32}")
)
func (b *Bdiscord) replaceChannelMentions(text string) string {
replaceChannelMentionFunc := func(match string) string {
var err error
channelID := match[2 : len(match)-1]
channelName := b.getChannelName(channelID)
// If we don't have the channel refresh our list.
if channelName == "" {
b.channels, err = b.c.GuildChannels(b.guildID)
if err != nil {
return "#unknownchannel"
}
channelName = b.getChannelName(channelID)
}
return "#" + channelName
}
return channelMentionRE.ReplaceAllStringFunc(text, replaceChannelMentionFunc)
}
func (b *Bdiscord) replaceUserMentions(text string) string {
replaceUserMentionFunc := func(match string) string {
var (
err error
member *discordgo.Member
username string
)
usernames := enumerateUsernames(match[1:])
for _, username = range usernames {
b.Log.Debugf("Testing mention: '%s'", username)
member, err = b.getGuildMemberByNick(username)
if err == nil {
break
}
}
if member == nil {
return match
}
return strings.Replace(match, "@"+username, member.User.Mention(), 1)
}
return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc)
}
func (b *Bdiscord) stripCustomoji(text string) string {
return emojiRE.ReplaceAllString(text, `$1`)
}
func (b *Bdiscord) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") {
return text[1:], true
}
return text, false
}
// splitURL splits a webhookURL and returns the ID and token.
func (b *Bdiscord) splitURL(url string) (string, string) {
const (
expectedWebhookSplitCount = 7
webhookIdxID = 5
webhookIdxToken = 6
)
webhookURLSplit := strings.Split(url, "/")
if len(webhookURLSplit) != expectedWebhookSplitCount {
b.Log.Fatalf("%s is no correct discord WebhookURL", url)
}
return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken]
}
func enumerateUsernames(s string) []string {
onlySpace := true
for _, r := range s {
if !unicode.IsSpace(r) {
onlySpace = false
break
}
}
if onlySpace {
return nil
}
var username, endSpace string
var usernames []string
skippingSpace := true
for _, r := range s {
if unicode.IsSpace(r) {
if !skippingSpace {
usernames = append(usernames, username)
skippingSpace = true
}
endSpace += string(r)
username += string(r)
} else {
endSpace = ""
username += string(r)
skippingSpace = false
}
}
if endSpace == "" {
usernames = append(usernames, username)
}
return usernames
}

View File

@ -0,0 +1,46 @@
package bdiscord
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEnumerateUsernames(t *testing.T) {
testcases := map[string]struct {
match string
expectedUsernames []string
}{
"only space": {
match: " \t\n \t",
expectedUsernames: nil,
},
"single word": {
match: "veni",
expectedUsernames: []string{"veni"},
},
"single word with preceeding space": {
match: " vidi",
expectedUsernames: []string{" vidi"},
},
"single word with suffixed space": {
match: "vici ",
expectedUsernames: []string{"vici"},
},
"multi-word with varying whitespace": {
match: "just me and\tmy friends \t",
expectedUsernames: []string{
"just",
"just me",
"just me and",
"just me and\tmy",
"just me and\tmy friends",
},
},
}
for testname, testcase := range testcases {
foundUsernames := enumerateUsernames(testcase.match)
assert.Equalf(t, testcase.expectedUsernames, foundUsernames, "Should have found the expected usernames for testcase %s", testname)
}
}

View File

@ -3,6 +3,7 @@ package helper
import (
"bytes"
"fmt"
"image/png"
"io"
"net/http"
"regexp"
@ -10,14 +11,19 @@ import (
"time"
"unicode/utf8"
"golang.org/x/image/webp"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"gitlab.com/golang-commonmark/markdown"
)
// DownloadFile downloads the given non-authenticated URL.
func DownloadFile(url string) (*[]byte, error) {
return DownloadFileAuth(url, "")
}
// DownloadFileAuth downloads the given URL using the specified authentication token.
func DownloadFileAuth(url string, auth string) (*[]byte, error) {
var buf bytes.Buffer
client := &http.Client{
@ -41,8 +47,8 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) {
}
// GetSubLines splits messages in newline-delimited lines. If maxLineLength is
// specified as non-zero GetSubLines will and also clip long lines to the
// maximum length and insert a warning marker that the line was clipped.
// specified as non-zero GetSubLines will also clip long lines to the maximum
// length and insert a warning marker that the line was clipped.
//
// TODO: The current implementation has the inconvenient that it disregards
// word boundaries when splitting but this is hard to solve without potentially
@ -78,18 +84,24 @@ func GetSubLines(message string, maxLineLength int) []string {
return lines
}
// handle all the stuff we put into extra
// HandleExtra manages the supplementary details stored inside a message's 'Extra' field map.
func HandleExtra(msg *config.Message, general *config.Protocol) []config.Message {
extra := msg.Extra
rmsg := []config.Message{}
for _, f := range extra[config.EventFileFailureSize] {
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, Account: msg.Account})
rmsg = append(rmsg, config.Message{
Text: text,
Username: "<system> ",
Channel: msg.Channel,
Account: msg.Account,
})
}
return rmsg
}
// GetAvatar constructs a URL for a given user-avatar if it is available in the cache.
func GetAvatar(av map[string]string, userid string, general *config.Protocol) string {
if sha, ok := av[userid]; ok {
return general.MediaServerDownload + "/" + sha + "/" + userid + ".png"
@ -97,13 +109,15 @@ func GetAvatar(av map[string]string, userid string, general *config.Protocol) st
return ""
}
func HandleDownloadSize(flog *log.Entry, msg *config.Message, name string, size int64, general *config.Protocol) error {
// HandleDownloadSize checks a specified filename against the configured download blacklist
// and checks a specified file-size against the configure limit.
func HandleDownloadSize(logger *logrus.Entry, msg *config.Message, name string, size int64, general *config.Protocol) error {
// check blacklist here
for _, entry := range general.MediaDownloadBlackList {
if entry != "" {
re, err := regexp.Compile(entry)
if err != nil {
flog.Errorf("incorrect regexp %s for %s", entry, msg.Account)
logger.Errorf("incorrect regexp %s for %s", entry, msg.Account)
continue
}
if re.MatchString(name) {
@ -111,43 +125,77 @@ func HandleDownloadSize(flog *log.Entry, msg *config.Message, name string, size
}
}
}
flog.Debugf("Trying to download %#v with size %#v", name, size)
logger.Debugf("Trying to download %#v with size %#v", name, size)
if int(size) > general.MediaDownloadSize {
msg.Event = config.EventFileFailureSize
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{Name: name, Comment: msg.Text, Size: size})
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{
Name: name,
Comment: msg.Text,
Size: size,
})
return fmt.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, general.MediaDownloadSize)
}
return nil
}
func HandleDownloadData(flog *log.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
var avatar bool
flog.Debugf("Download OK %#v %#v", name, len(*data))
logger.Debugf("Download OK %#v %#v", name, len(*data))
if msg.Event == config.EventAvatarDownload {
avatar = true
}
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, URL: url, Comment: comment, Avatar: avatar})
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{
Name: name,
Data: data,
URL: url,
Comment: comment,
Avatar: avatar,
})
}
var emptyLineMatcher = regexp.MustCompile("\n+")
// RemoveEmptyNewLines collapses consecutive newline characters into a single one and
// trims any preceding or trailing newline characters as well.
func RemoveEmptyNewLines(msg string) string {
lines := ""
for _, line := range strings.Split(msg, "\n") {
if line != "" {
lines += line + "\n"
}
}
lines = strings.TrimRight(lines, "\n")
return lines
return emptyLineMatcher.ReplaceAllString(strings.Trim(msg, "\n"), "\n")
}
// ClipMessage trims a message to the specified length if it exceeds it and adds a warning
// to the message in case it does so.
func ClipMessage(text string, length int) string {
// clip too long messages
const clippingMessage = " <clipped message>"
if len(text) > length {
text = text[:length-len(" *message clipped*")]
text = text[:length-len(clippingMessage)]
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
text = text[:len(text)-size]
}
text += " *message clipped*"
text += clippingMessage
}
return text
}
func ParseMarkdown(input string) string {
md := markdown.New(markdown.XHTMLOutput(true), markdown.Breaks(true))
res := md.RenderToString([]byte(input))
res = strings.TrimPrefix(res, "<p>")
res = strings.TrimSuffix(res, "</p>\n")
return res
}
// ConvertWebPToPNG convert input data (which should be WebP format to PNG format)
func ConvertWebPToPNG(data *[]byte) error {
r := bytes.NewReader(*data)
m, err := webp.Decode(r)
if err != nil {
return err
}
var output []byte
w := bytes.NewBuffer(output)
if err := png.Encode(w, m); err != nil {
return err
}
*data = w.Bytes()
return nil
}

View File

@ -1,6 +1,8 @@
package helper
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
@ -103,3 +105,22 @@ func TestGetSubLines(t *testing.T) {
assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname)
}
}
func TestConvertWebPToPNG(t *testing.T) {
if os.Getenv("LOCAL_TEST") == "" {
t.Skip()
}
input, err := ioutil.ReadFile("test.webp")
if err != nil {
t.Fail()
}
d := &input
err = ConvertWebPToPNG(d)
if err != nil {
t.Fail()
}
err = ioutil.WriteFile("test.png", *d, 0644)
if err != nil {
t.Fail()
}
}

239
bridge/irc/handlers.go Normal file
View File

@ -0,0 +1,239 @@
package birc
import (
"bytes"
"fmt"
"io/ioutil"
"regexp"
"strconv"
"strings"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/dfordsoft/golib/ic"
"github.com/lrstanley/girc"
"github.com/paulrosania/go-charset/charset"
"github.com/saintfish/chardet"
// We need to import the 'data' package as an implicit dependency.
// See: https://godoc.org/github.com/paulrosania/go-charset/charset
_ "github.com/paulrosania/go-charset/data"
)
func (b *Birc) handleCharset(msg *config.Message) error {
if b.GetString("Charset") != "" {
switch b.GetString("Charset") {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text)
default:
buf := new(bytes.Buffer)
w, err := charset.NewWriter(b.GetString("Charset"), buf)
if err != nil {
b.Log.Errorf("charset from utf-8 conversion failed: %s", err)
return err
}
fmt.Fprint(w, msg.Text)
w.Close()
msg.Text = buf.String()
}
}
return nil
}
// handleFiles returns true if we have handled the files, otherwise return false
func (b *Birc) handleFiles(msg *config.Message) bool {
if msg.Extra == nil {
return false
}
for _, rmsg := range helper.HandleExtra(msg, b.General) {
b.Local <- rmsg
}
if len(msg.Extra["file"]) == 0 {
return false
}
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
}
return true
}
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
if len(event.Params) == 0 {
b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
return
}
channel := strings.ToLower(event.Params[0])
if event.Command == "KICK" && event.Params[1] == b.Nick {
b.Log.Infof("Got kicked from %s by %s", channel, event.Source.Name)
time.Sleep(time.Duration(b.GetInt("RejoinDelay")) * time.Second)
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EventRejoinChannels}
return
}
if event.Command == "QUIT" {
if event.Source.Name == b.Nick && strings.Contains(event.Last(), "Ping timeout") {
b.Log.Infof("%s reconnecting ..", b.Account)
b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EventFailure}
return
}
}
if event.Source.Name != b.Nick {
if b.GetBool("nosendjoinpart") {
return
}
b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account)
// QUIT isn't channel bound, happens for all channels on the bridge
if event.Command == "QUIT" {
channel = ""
}
msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
b.Log.Debugf("<= Message is %#v", msg)
b.Remote <- msg
return
}
b.Log.Debugf("handle %#v", event)
}
func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
b.Log.Debug("Registering callbacks")
i := b.i
b.Nick = event.Params[0]
i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg)
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.Handlers.Add(girc.NOTICE, b.handleNotice)
i.Handlers.Add("JOIN", b.handleJoinPart)
i.Handlers.Add("PART", b.handleJoinPart)
i.Handlers.Add("QUIT", b.handleJoinPart)
i.Handlers.Add("KICK", b.handleJoinPart)
}
func (b *Birc) handleNickServ() {
if !b.GetBool("UseSASL") && b.GetString("NickServNick") != "" && b.GetString("NickServPassword") != "" {
b.Log.Debugf("Sending identify to nickserv %s", b.GetString("NickServNick"))
b.i.Cmd.Message(b.GetString("NickServNick"), "IDENTIFY "+b.GetString("NickServPassword"))
}
if strings.EqualFold(b.GetString("NickServNick"), "Q@CServe.quakenet.org") {
b.Log.Debugf("Authenticating %s against %s", b.GetString("NickServUsername"), b.GetString("NickServNick"))
b.i.Cmd.Message(b.GetString("NickServNick"), "AUTH "+b.GetString("NickServUsername")+" "+b.GetString("NickServPassword"))
}
// give nickserv some slack
time.Sleep(time.Second * 5)
b.authDone = true
}
func (b *Birc) handleNotice(client *girc.Client, event girc.Event) {
if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.GetString("NickServNick") {
b.handleNickServ()
} else {
b.handlePrivMsg(client, event)
}
}
func (b *Birc) handleOther(client *girc.Client, event girc.Event) {
if b.GetInt("DebugLevel") == 1 {
if event.Command != "CLIENT_STATE_UPDATED" &&
event.Command != "CLIENT_GENERAL_UPDATED" {
b.Log.Debugf("%#v", event.String())
}
return
}
switch event.Command {
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
return
}
b.Log.Debugf("%#v", event.String())
}
func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
b.handleNickServ()
b.handleRunCommands()
// we are now fully connected
b.connected <- nil
}
func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
if b.skipPrivMsg(event) {
return
}
rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
// set action event
if event.IsAction() {
rmsg.Event = config.EventUserAction
}
// strip action, we made an event if it was an action
rmsg.Text += event.StripAction()
// strip IRC colors
re := regexp.MustCompile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
rmsg.Text = re.ReplaceAllString(rmsg.Text, "")
// start detecting the charset
mycharset := b.GetString("Charset")
if mycharset == "" {
// detect what were sending so that we convert it to utf-8
detector := chardet.NewTextDetector()
result, err := detector.DetectBest([]byte(rmsg.Text))
if err != nil {
b.Log.Infof("detection failed for rmsg.Text: %#v", rmsg.Text)
return
}
b.Log.Debugf("detected %s confidence %#v", result.Charset, result.Confidence)
mycharset = result.Charset
// if we're not sure, just pick ISO-8859-1
if result.Confidence < 80 {
mycharset = "ISO-8859-1"
}
}
switch mycharset {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text)
default:
r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
if err != nil {
b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
return
}
output, _ := ioutil.ReadAll(r)
rmsg.Text = string(output)
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account)
b.Remote <- rmsg
}
func (b *Birc) handleRunCommands() {
for _, cmd := range b.GetStringSlice("RunCommands") {
if err := b.i.Cmd.SendRaw(cmd); err != nil {
b.Log.Errorf("RunCommands %s failed: %s", cmd, err)
}
time.Sleep(time.Second)
}
}
func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) {
parts := strings.Split(event.Params[2], "!")
t, err := strconv.ParseInt(event.Params[3], 10, 64)
if err != nil {
b.Log.Errorf("Invalid time stamp: %s", event.Params[3])
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
b.Log.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0))
}

View File

@ -1,14 +1,10 @@
package birc
import (
"bytes"
"crypto/tls"
"fmt"
"hash/crc32"
"io"
"io/ioutil"
"net"
"regexp"
"sort"
"strconv"
"strings"
@ -17,10 +13,7 @@ import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/dfordsoft/golib/ic"
"github.com/lrstanley/girc"
"github.com/paulrosania/go-charset/charset"
"github.com/saintfish/chardet"
// We need to import the 'data' package as an implicit dependency.
// See: https://godoc.org/github.com/paulrosania/go-charset/charset
@ -68,7 +61,7 @@ func (b *Birc) Command(msg *config.Message) string {
if msg.Text == "!users" {
b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames)
b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames)
b.i.Cmd.SendRaw("NAMES " + msg.Channel)
b.i.Cmd.SendRaw("NAMES " + msg.Channel) //nolint:errcheck
}
return ""
}
@ -76,35 +69,11 @@ func (b *Birc) Command(msg *config.Message) string {
func (b *Birc) Connect() error {
b.Local = make(chan config.Message, b.MessageQueue+10)
b.Log.Infof("Connecting %s", b.GetString("Server"))
server, portstr, err := net.SplitHostPort(b.GetString("Server"))
if err != nil {
return err
}
port, err := strconv.Atoi(portstr)
if err != nil {
return err
}
// fix strict user handling of girc
user := b.GetString("Nick")
for !girc.IsValidUser(user) {
if len(user) == 1 {
user = "matterbridge"
break
}
user = user[1:]
}
i := girc.New(girc.Config{
Server: server,
ServerPass: b.GetString("Password"),
Port: port,
Nick: b.GetString("Nick"),
User: user,
Name: b.GetString("Nick"),
SSL: b.GetBool("UseTLS"),
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server},
PingDelay: time.Minute,
})
i, err := b.getClient()
if err != nil {
return err
}
if b.GetBool("UseSASL") {
i.Config.SASL = &girc.SASLPlain{
@ -115,30 +84,12 @@ func (b *Birc) Connect() error {
i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection)
i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
i.Handlers.Add(girc.ERR_NOMOTD, b.handleOtherAuth)
i.Handlers.Add(girc.ALL_EVENTS, b.handleOther)
go func() {
for {
if err := i.Connect(); err != nil {
b.Log.Errorf("disconnect: error: %s", err)
if b.FirstConnection {
b.connected <- err
return
}
} else {
b.Log.Info("disconnect: client requested quit")
}
b.Log.Info("reconnecting in 30 seconds...")
time.Sleep(30 * time.Second)
i.Handlers.Clear(girc.RPL_WELCOME)
i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) {
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels}
// set our correct nick on reconnect if necessary
b.Nick = event.Source.Name
})
}
}()
b.i = i
go b.doConnect()
err = <-b.connected
if err != nil {
return fmt.Errorf("connection failed %s", err)
@ -172,7 +123,6 @@ func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
} else {
b.i.Cmd.Join(channel.Name)
}
b.authDone = false
return nil
}
@ -195,44 +145,13 @@ func (b *Birc) Send(msg config.Message) (string, error) {
}
// convert to specified charset
if b.GetString("Charset") != "" {
switch b.GetString("Charset") {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text)
default:
buf := new(bytes.Buffer)
w, err := charset.NewWriter(b.GetString("Charset"), buf)
if err != nil {
b.Log.Errorf("charset from utf-8 conversion failed: %s", err)
return "", err
}
fmt.Fprint(w, msg.Text)
w.Close()
msg.Text = buf.String()
}
if err := b.handleCharset(&msg); err != nil {
return "", err
}
// Handle files
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
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
}
return "", nil
}
// handle files, return if we're done here
if ok := b.handleFiles(&msg); ok {
return "", nil
}
var msgLines []string
@ -257,6 +176,28 @@ func (b *Birc) Send(msg config.Message) (string, error) {
return "", nil
}
func (b *Birc) doConnect() {
for {
if err := b.i.Connect(); err != nil {
b.Log.Errorf("disconnect: error: %s", err)
if b.FirstConnection {
b.connected <- err
return
}
} else {
b.Log.Info("disconnect: client requested quit")
}
b.Log.Info("reconnecting in 30 seconds...")
time.Sleep(30 * time.Second)
b.i.Handlers.Clear(girc.RPL_WELCOME)
b.i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) {
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels}
// set our correct nick on reconnect if necessary
b.Nick = event.Source.Name
})
}
}
func (b *Birc) doSend() {
rate := time.Millisecond * time.Duration(b.MessageDelay)
throttle := time.NewTicker(rate)
@ -277,6 +218,40 @@ func (b *Birc) doSend() {
}
}
// validateInput validates the server/port/nick configuration. Returns a *girc.Client if successful
func (b *Birc) getClient() (*girc.Client, error) {
server, portstr, err := net.SplitHostPort(b.GetString("Server"))
if err != nil {
return nil, err
}
port, err := strconv.Atoi(portstr)
if err != nil {
return nil, err
}
// fix strict user handling of girc
user := b.GetString("Nick")
for !girc.IsValidUser(user) {
if len(user) == 1 {
user = "matterbridge"
break
}
user = user[1:]
}
i := girc.New(girc.Config{
Server: server,
ServerPass: b.GetString("Password"),
Port: port,
Nick: b.GetString("Nick"),
User: user,
Name: b.GetString("Nick"),
SSL: b.GetBool("UseTLS"),
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec
PingDelay: time.Minute,
})
return i, nil
}
func (b *Birc) endNames(client *girc.Client, event girc.Event) {
channel := event.Params[1]
sort.Strings(b.names[channel])
@ -293,82 +268,6 @@ func (b *Birc) endNames(client *girc.Client, event girc.Event) {
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
}
func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
b.Log.Debug("Registering callbacks")
i := b.i
b.Nick = event.Params[0]
i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg)
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.Handlers.Add(girc.NOTICE, b.handleNotice)
i.Handlers.Add("JOIN", b.handleJoinPart)
i.Handlers.Add("PART", b.handleJoinPart)
i.Handlers.Add("QUIT", b.handleJoinPart)
i.Handlers.Add("KICK", b.handleJoinPart)
}
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
if len(event.Params) == 0 {
b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
return
}
channel := strings.ToLower(event.Params[0])
if event.Command == "KICK" && event.Params[1] == b.Nick {
b.Log.Infof("Got kicked from %s by %s", channel, event.Source.Name)
time.Sleep(time.Duration(b.GetInt("RejoinDelay")) * time.Second)
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EventRejoinChannels}
return
}
if event.Command == "QUIT" {
if event.Source.Name == b.Nick && strings.Contains(event.Trailing, "Ping timeout") {
b.Log.Infof("%s reconnecting ..", b.Account)
b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EventFailure}
return
}
}
if event.Source.Name != b.Nick {
if b.GetBool("nosendjoinpart") {
return
}
b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account)
msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
b.Log.Debugf("<= Message is %#v", msg)
b.Remote <- msg
return
}
b.Log.Debugf("handle %#v", event)
}
func (b *Birc) handleNotice(client *girc.Client, event girc.Event) {
if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.GetString("NickServNick") {
b.handleNickServ()
} else {
b.handlePrivMsg(client, event)
}
}
func (b *Birc) handleOther(client *girc.Client, event girc.Event) {
if b.GetInt("DebugLevel") == 1 {
if event.Command != "CLIENT_STATE_UPDATED" &&
event.Command != "CLIENT_GENERAL_UPDATED" {
b.Log.Debugf("%#v", event.String())
}
return
}
switch event.Command {
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
return
}
b.Log.Debugf("%#v", event.String())
}
func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
b.handleNickServ()
// we are now fully connected
b.connected <- nil
}
func (b *Birc) skipPrivMsg(event girc.Event) bool {
// Our nick can be changed
b.Nick = b.i.GetNick()
@ -388,74 +287,6 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
return false
}
func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
if b.skipPrivMsg(event) {
return
}
rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Trailing, event)
// set action event
if event.IsAction() {
rmsg.Event = config.EventUserAction
}
// strip action, we made an event if it was an action
rmsg.Text += event.StripAction()
// strip IRC colors
re := regexp.MustCompile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
rmsg.Text = re.ReplaceAllString(rmsg.Text, "")
// start detecting the charset
var r io.Reader
var err error
mycharset := b.GetString("Charset")
if mycharset == "" {
// detect what were sending so that we convert it to utf-8
detector := chardet.NewTextDetector()
result, err := detector.DetectBest([]byte(rmsg.Text))
if err != nil {
b.Log.Infof("detection failed for rmsg.Text: %#v", rmsg.Text)
return
}
b.Log.Debugf("detected %s confidence %#v", result.Charset, result.Confidence)
mycharset = result.Charset
// if we're not sure, just pick ISO-8859-1
if result.Confidence < 80 {
mycharset = "ISO-8859-1"
}
}
switch mycharset {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text)
default:
r, err = charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
if err != nil {
b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
return
}
output, _ := ioutil.ReadAll(r)
rmsg.Text = string(output)
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account)
b.Remote <- rmsg
}
func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) {
parts := strings.Split(event.Params[2], "!")
t, err := strconv.ParseInt(event.Params[3], 10, 64)
if err != nil {
b.Log.Errorf("Invalid time stamp: %s", event.Params[3])
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
b.Log.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0))
}
func (b *Birc) nicksPerRow() int {
return 4
}
@ -464,23 +295,9 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
channel := event.Params[2]
b.names[channel] = append(
b.names[channel],
strings.Split(strings.TrimSpace(event.Trailing), " ")...)
strings.Split(strings.TrimSpace(event.Last()), " ")...)
}
func (b *Birc) formatnicks(nicks []string) string {
return strings.Join(nicks, ", ") + " currently on IRC"
}
func (b *Birc) handleNickServ() {
if !b.GetBool("UseSASL") && b.GetString("NickServNick") != "" && b.GetString("NickServPassword") != "" {
b.Log.Debugf("Sending identify to nickserv %s", b.GetString("NickServNick"))
b.i.Cmd.Message(b.GetString("NickServNick"), "IDENTIFY "+b.GetString("NickServPassword"))
}
if strings.EqualFold(b.GetString("NickServNick"), "Q@CServe.quakenet.org") {
b.Log.Debugf("Authenticating %s against %s", b.GetString("NickServUsername"), b.GetString("NickServNick"))
b.i.Cmd.Message(b.GetString("NickServNick"), "AUTH "+b.GetString("NickServUsername")+" "+b.GetString("NickServPassword"))
}
// give nickserv some slack
time.Sleep(time.Second * 5)
b.authDone = true
}

View File

@ -3,6 +3,7 @@ package bmatrix
import (
"bytes"
"fmt"
"html"
"mime"
"regexp"
"strings"
@ -19,11 +20,13 @@ type Bmatrix struct {
UserID string
RoomMap map[string]string
sync.RWMutex
htmlTag *regexp.Regexp
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bmatrix{Config: cfg}
b.htmlTag = regexp.MustCompile("</.*?>")
b.RoomMap = make(map[string]string)
return b
}
@ -99,19 +102,35 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.mc.SendText(channel, rmsg.Username+rmsg.Text)
if _, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text); err != nil {
b.Log.Errorf("sendText failed: %s", err)
}
}
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg, channel)
return b.handleUploadFiles(&msg, channel)
}
}
// Edit message if we have an ID
// matrix has no editing support
// Post normal message
resp, err := b.mc.SendText(channel, msg.Username+msg.Text)
// Use notices to send join/leave events
if msg.Event == config.EventJoinLeave {
resp, err := b.mc.SendNotice(channel, msg.Username+msg.Text)
if err != nil {
return "", err
}
return resp.EventID, err
}
username := html.EscapeString(msg.Username)
// check if we have a </tag>. if we have, we don't escape HTML. #696
if b.htmlTag.MatchString(msg.Username) {
username = msg.Username
}
// Post normal message with HTML support (eg riot.im)
resp, err := b.mc.SendHTML(channel, msg.Username+msg.Text, username+helper.ParseMarkdown(msg.Text))
if err != nil {
return "", err
}
@ -257,47 +276,60 @@ func (b *Bmatrix) handleDownloadFile(rmsg *config.Message, content map[string]in
return nil
}
// handleUploadFile handles native upload of files
func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string) (string, error) {
// handleUploadFiles handles native upload of files.
func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string, error) {
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 {
b.Log.Errorf("file comment failed: %#v", err)
}
}
b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
if err != nil {
b.Log.Errorf("file upload failed: %#v", err)
continue
}
if strings.Contains(mtype, "video") {
b.Log.Debugf("sendVideo %s", res.ContentURI)
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
if err != nil {
b.Log.Errorf("sendVideo failed: %#v", err)
}
}
if strings.Contains(mtype, "image") {
b.Log.Debugf("sendImage %s", res.ContentURI)
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
if err != nil {
b.Log.Errorf("sendImage failed: %#v", err)
}
}
b.Log.Debugf("result: %#v", res)
if fi, ok := f.(config.FileInfo); ok {
b.handleUploadFile(msg, channel, &fi)
}
}
return "", nil
}
// handleUploadFile handles native upload of a file.
func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *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") {
return
}
if fi.Comment != "" {
_, err := b.mc.SendText(channel, msg.Username+fi.Comment)
if err != nil {
b.Log.Errorf("file comment failed: %#v", err)
}
} else {
// image and video uploads send no username, we have to do this ourself here #715
_, err := b.mc.SendText(channel, msg.Username)
if err != nil {
b.Log.Errorf("file comment failed: %#v", err)
}
}
b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
if err != nil {
b.Log.Errorf("file upload failed: %#v", err)
return
}
switch {
case strings.Contains(mtype, "video"):
b.Log.Debugf("sendVideo %s", res.ContentURI)
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
if err != nil {
b.Log.Errorf("sendVideo failed: %#v", err)
}
case strings.Contains(mtype, "image"):
b.Log.Debugf("sendImage %s", res.ContentURI)
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
if err != nil {
b.Log.Errorf("sendImage failed: %#v", err)
}
}
b.Log.Debugf("result: %#v", res)
}
// skipMessages returns true if this message should not be handled
func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool {
// Skip empty messages

View File

@ -0,0 +1,195 @@
package bmattermost
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient"
"github.com/mattermost/mattermost-server/model"
)
// 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) {
rmsg := config.Message{
Username: "system",
Text: "avatar",
Channel: channel,
Account: b.Account,
UserID: userid,
Event: config.EventAvatarDownload,
Extra: make(map[string][]interface{}),
}
if _, ok := b.avatarMap[userid]; !ok {
data, resp := b.mc.Client.GetProfileImage(userid, "")
if resp.Error != nil {
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
return
}
err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
if err != nil {
b.Log.Error(err)
return
}
helper.HandleDownloadData(b.Log, &rmsg, userid+".png", rmsg.Text, "", &data, b.General)
b.Remote <- rmsg
}
}
// handleDownloadFile handles file download
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
url, _ := b.mc.Client.GetFileLink(id)
finfo, resp := b.mc.Client.GetFileInfo(id)
if resp.Error != nil {
return resp.Error
}
err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
if err != nil {
return err
}
data, resp := b.mc.Client.DownloadFile(id, true)
if resp.Error != nil {
return resp.Error
}
helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
return nil
}
func (b *Bmattermost) handleMatter() {
messages := make(chan *config.Message)
if b.GetString("WebhookBindAddress") != "" {
b.Log.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(messages)
} else {
if b.GetString("Token") != "" {
b.Log.Debugf("Choosing token based receiving")
} else {
b.Log.Debugf("Choosing login/password based receiving")
}
go b.handleMatterClient(messages)
}
var ok bool
for message := range messages {
message.Avatar = helper.GetAvatar(b.avatarMap, message.UserID, b.General)
message.Account = b.Account
message.Text, ok = b.replaceAction(message.Text)
if ok {
message.Event = config.EventUserAction
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
b.Log.Debugf("<= Message is %#v", message)
b.Remote <- *message
}
}
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
for message := range b.mc.MessageChan {
b.Log.Debugf("%#v", message.Raw.Data)
if b.skipMessage(message) {
b.Log.Debugf("Skipped message: %#v", message)
continue
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
b.handleDownloadAvatar(message.UserID, message.Channel)
}
b.Log.Debugf("== Receiving event %#v", message)
rmsg := &config.Message{
Username: message.Username,
UserID: message.UserID,
Channel: message.Channel,
Text: message.Text,
ID: message.Post.Id,
ParentID: message.Post.ParentId,
Extra: make(map[string][]interface{}),
}
// handle mattermost post properties (override username and attachments)
b.handleProps(rmsg, message)
// create a text for bridges that don't support native editing
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") {
rmsg.Text = message.Text + b.GetString("EditSuffix")
}
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED {
rmsg.Event = config.EventMsgDelete
}
for _, id := range message.Post.FileIds {
err := b.handleDownloadFile(rmsg, id)
if err != nil {
b.Log.Errorf("download failed: %s", err)
}
}
// Use nickname instead of username if defined
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
rmsg.Username = nick
}
messages <- rmsg
}
}
func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
for {
message := b.mh.Receive()
b.Log.Debugf("Receiving from matterhook %#v", message)
messages <- &config.Message{
UserID: message.UserID,
Username: message.UserName,
Text: message.Text,
Channel: message.ChannelName,
}
}
}
// handleUploadFile handles native upload of files
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
var err error
var res, id string
channelID := b.mc.GetChannelId(msg.Channel, b.TeamID)
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
if err != nil {
return "", err
}
msg.Text = fi.Comment
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
res, err = b.mc.PostMessageWithFiles(channelID, msg.Text, msg.ParentID, []string{id})
}
return res, err
}
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
props := message.Post.Props
if props == nil {
return
}
if _, ok := props["override_username"].(string); ok {
rmsg.Username = props["override_username"].(string)
}
if _, ok := props["attachments"].([]interface{}); ok {
rmsg.Extra["attachments"] = props["attachments"].([]interface{})
if rmsg.Text == "" {
for _, attachment := range rmsg.Extra["attachments"] {
attach := attachment.(map[string]interface{})
if attach["text"].(string) != "" {
rmsg.Text += attach["text"].(string)
continue
}
if attach["fallback"].(string) != "" {
rmsg.Text += attach["fallback"].(string)
}
}
}
}
}

View File

@ -0,0 +1,218 @@
package bmattermost
import (
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook"
"github.com/mattermost/mattermost-server/model"
)
func (b *Bmattermost) doConnectWebhookBind() error {
switch {
case b.GetString("WebhookURL") != "":
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
BindAddress: b.GetString("WebhookBindAddress")})
case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending)")
err := b.apiLogin()
if err != nil {
return err
}
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending)")
err := b.apiLogin()
if err != nil {
return err
}
default:
b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
BindAddress: b.GetString("WebhookBindAddress")})
}
return nil
}
func (b *Bmattermost) doConnectWebhookURL() error {
b.Log.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true})
if b.GetString("Token") != "" {
b.Log.Info("Connecting using token (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
} else if b.GetString("Login") != "" {
b.Log.Info("Connecting using login/password (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
}
return nil
}
func (b *Bmattermost) apiLogin() error {
password := b.GetString("Password")
if b.GetString("Token") != "" {
password = "token=" + b.GetString("Token")
}
b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"))
if b.GetBool("debug") {
b.mc.SetLogLevel("debug")
}
b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify")
b.mc.NoTLS = b.GetBool("NoTLS")
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
err := b.mc.Login()
if err != nil {
return err
}
b.Log.Info("Connection succeeded")
b.TeamID = b.mc.GetTeamId()
go b.mc.WsReceiver()
go b.mc.StatusLoop()
return nil
}
// replaceAction replace the message with the correct action (/me) code
func (b *Bmattermost) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
return strings.Replace(text, "*", "", -1), true
}
return text, false
}
func (b *Bmattermost) cacheAvatar(msg *config.Message) (string, error) {
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 != "" {
b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
b.avatarMap[msg.UserID] = fi.SHA
}
return "", nil
}
// sendWebhook uses the configured WebhookURL to send the message
func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
// skip events
if msg.Event != "" {
return "", nil
}
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
if msg.Extra != nil {
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
rmsg := rmsg // scopelint
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Channel: rmsg.Channel,
UserName: rmsg.Username,
Text: rmsg.Text,
Props: make(map[string]interface{}),
}
matterMessage.Props["matterbridge_"+b.uuid] = true
if err := b.mh.Send(matterMessage); err != nil {
b.Log.Errorf("sendWebhook failed: %s ", err)
}
}
// webhook doesn't support file uploads, so we add the url manually
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += fi.URL
}
}
}
}
iconURL := config.GetIconURL(&msg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Channel: msg.Channel,
UserName: msg.Username,
Text: msg.Text,
Props: make(map[string]interface{}),
}
if msg.Avatar != "" {
matterMessage.IconURL = msg.Avatar
}
matterMessage.Props["matterbridge_"+b.uuid] = true
err := b.mh.Send(matterMessage)
if err != nil {
b.Log.Info(err)
return "", err
}
return "", nil
}
// skipMessages returns true if this message should not be handled
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
// Handle join/leave
if message.Type == "system_join_leave" ||
message.Type == "system_join_channel" ||
message.Type == "system_leave_channel" {
if b.GetBool("nosendjoinpart") {
return true
}
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{
Username: "system",
Text: message.Text,
Channel: message.Channel,
Account: b.Account,
Event: config.EventJoinLeave,
}
return true
}
// Handle edited messages
if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") {
return true
}
// Ignore messages sent from matterbridge
if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
b.Log.Debugf("sent by matterbridge, ignoring")
return true
}
}
// Ignore messages sent from a user logged in as the bot
if b.mc.User.Username == message.Username {
return true
}
// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
if message.Post.HasReactions {
return true
}
// ignore messages from other teams than ours
if message.Raw.Data["team_id"].(string) != b.TeamID {
return true
}
// only handle posted, edited or deleted events
if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED ||
message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) {
return true
}
return false
}

View File

@ -3,14 +3,12 @@ package bmattermost
import (
"errors"
"fmt"
"strings"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook"
"github.com/mattermost/platform/model"
"github.com/rs/xid"
)
@ -40,54 +38,18 @@ func (b *Bmattermost) Connect() error {
return nil
}
if b.GetString("WebhookBindAddress") != "" {
switch {
case b.GetString("WebhookURL") != "":
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
BindAddress: b.GetString("WebhookBindAddress")})
case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending)")
err := b.apiLogin()
if err != nil {
return err
}
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending)")
err := b.apiLogin()
if err != nil {
return err
}
default:
b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
BindAddress: b.GetString("WebhookBindAddress")})
if err := b.doConnectWebhookBind(); err != nil {
return err
}
go b.handleMatter()
return nil
}
switch {
case b.GetString("WebhookURL") != "":
b.Log.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true})
if b.GetString("Token") != "" {
b.Log.Info("Connecting using token (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
} else if b.GetString("Login") != "" {
b.Log.Info("Connecting using login/password (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
if err := b.doConnectWebhookURL(); err != nil {
return err
}
go b.handleMatter()
return nil
case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending and receiving)")
@ -104,7 +66,8 @@ func (b *Bmattermost) Connect() error {
}
go b.handleMatter()
}
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" && b.GetString("Login") == "" && b.GetString("Token") == "" {
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" &&
b.GetString("Login") == "" && b.GetString("Token") == "" {
return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token/Login/Password/Server/Team configured")
}
return nil
@ -161,7 +124,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text)
if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
b.Log.Errorf("PostMessage failed: %s", err)
}
}
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg)
@ -179,304 +144,5 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
}
// Post normal message
return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text)
}
func (b *Bmattermost) handleMatter() {
messages := make(chan *config.Message)
if b.GetString("WebhookBindAddress") != "" {
b.Log.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(messages)
} else {
if b.GetString("Token") != "" {
b.Log.Debugf("Choosing token based receiving")
} else {
b.Log.Debugf("Choosing login/password based receiving")
}
go b.handleMatterClient(messages)
}
var ok bool
for message := range messages {
message.Avatar = helper.GetAvatar(b.avatarMap, message.UserID, b.General)
message.Account = b.Account
message.Text, ok = b.replaceAction(message.Text)
if ok {
message.Event = config.EventUserAction
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
b.Log.Debugf("<= Message is %#v", message)
b.Remote <- *message
}
}
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
for message := range b.mc.MessageChan {
b.Log.Debugf("%#v", message.Raw.Data)
if b.skipMessage(message) {
b.Log.Debugf("Skipped message: %#v", message)
continue
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
b.handleDownloadAvatar(message.UserID, message.Channel)
}
b.Log.Debugf("== Receiving event %#v", message)
rmsg := &config.Message{Username: message.Username, UserID: message.UserID, Channel: message.Channel, Text: message.Text, ID: message.Post.Id, Extra: make(map[string][]interface{})}
// handle mattermost post properties (override username and attachments)
props := message.Post.Props
if props != nil {
if _, ok := props["override_username"].(string); ok {
rmsg.Username = props["override_username"].(string)
}
if _, ok := props["attachments"].([]interface{}); ok {
rmsg.Extra["attachments"] = props["attachments"].([]interface{})
if rmsg.Text == "" {
for _, attachment := range rmsg.Extra["attachments"] {
attach := attachment.(map[string]interface{})
if attach["text"].(string) != "" {
rmsg.Text += attach["text"].(string)
continue
}
if attach["fallback"].(string) != "" {
rmsg.Text += attach["fallback"].(string)
}
}
}
}
}
// create a text for bridges that don't support native editing
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") {
rmsg.Text = message.Text + b.GetString("EditSuffix")
}
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED {
rmsg.Event = config.EventMsgDelete
}
if len(message.Post.FileIds) > 0 {
for _, id := range message.Post.FileIds {
err := b.handleDownloadFile(rmsg, id)
if err != nil {
b.Log.Errorf("download failed: %s", err)
}
}
}
// Use nickname instead of username if defined
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
rmsg.Username = nick
}
messages <- rmsg
}
}
func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
for {
message := b.mh.Receive()
b.Log.Debugf("Receiving from matterhook %#v", message)
messages <- &config.Message{UserID: message.UserID, Username: message.UserName, Text: message.Text, Channel: message.ChannelName}
}
}
func (b *Bmattermost) apiLogin() error {
password := b.GetString("Password")
if b.GetString("Token") != "" {
password = "token=" + b.GetString("Token")
}
b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"))
if b.GetBool("debug") {
b.mc.SetLogLevel("debug")
}
b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify")
b.mc.NoTLS = b.GetBool("NoTLS")
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
err := b.mc.Login()
if err != nil {
return err
}
b.Log.Info("Connection succeeded")
b.TeamID = b.mc.GetTeamId()
go b.mc.WsReceiver()
go b.mc.StatusLoop()
return nil
}
// replaceAction replace the message with the correct action (/me) code
func (b *Bmattermost) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
return strings.Replace(text, "*", "", -1), true
}
return text, false
}
func (b *Bmattermost) cacheAvatar(msg *config.Message) (string, error) {
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 != "" {
b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
b.avatarMap[msg.UserID] = fi.SHA
}
return "", nil
}
// 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) {
rmsg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: userid, Event: config.EventAvatarDownload, Extra: make(map[string][]interface{})}
if _, ok := b.avatarMap[userid]; !ok {
data, resp := b.mc.Client.GetProfileImage(userid, "")
if resp.Error != nil {
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
return
}
err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
if err != nil {
b.Log.Error(err)
return
}
helper.HandleDownloadData(b.Log, &rmsg, userid+".png", rmsg.Text, "", &data, b.General)
b.Remote <- rmsg
}
}
// handleDownloadFile handles file download
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
url, _ := b.mc.Client.GetFileLink(id)
finfo, resp := b.mc.Client.GetFileInfo(id)
if resp.Error != nil {
return resp.Error
}
err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
if err != nil {
return err
}
data, resp := b.mc.Client.DownloadFile(id, true)
if resp.Error != nil {
return resp.Error
}
helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
return nil
}
// handleUploadFile handles native upload of files
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
var err error
var res, id string
channelID := b.mc.GetChannelId(msg.Channel, b.TeamID)
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
if err != nil {
return "", err
}
msg.Text = fi.Comment
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
res, err = b.mc.PostMessageWithFiles(channelID, msg.Text, []string{id})
}
return res, err
}
// sendWebhook uses the configured WebhookURL to send the message
func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
// skip events
if msg.Event != "" {
return "", nil
}
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
if msg.Extra != nil {
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
rmsg := rmsg // scopelint
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: rmsg.Channel, UserName: rmsg.Username, Text: rmsg.Text, Props: make(map[string]interface{})}
matterMessage.Props["matterbridge_"+b.uuid] = true
b.mh.Send(matterMessage)
}
// webhook doesn't support file uploads, so we add the url manually
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += fi.URL
}
}
}
}
iconURL := config.GetIconURL(&msg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: msg.Channel, UserName: msg.Username, Text: msg.Text, Props: make(map[string]interface{})}
if msg.Avatar != "" {
matterMessage.IconURL = msg.Avatar
}
matterMessage.Props["matterbridge_"+b.uuid] = true
err := b.mh.Send(matterMessage)
if err != nil {
b.Log.Info(err)
return "", err
}
return "", nil
}
// skipMessages returns true if this message should not be handled
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
// Handle join/leave
if message.Type == "system_join_leave" ||
message.Type == "system_join_channel" ||
message.Type == "system_leave_channel" {
if b.GetBool("nosendjoinpart") {
return true
}
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{Username: "system", Text: message.Text, Channel: message.Channel, Account: b.Account, Event: config.EventJoinLeave}
return true
}
// Handle edited messages
if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") {
return true
}
// Ignore messages sent from matterbridge
if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
b.Log.Debugf("sent by matterbridge, ignoring")
return true
}
}
// Ignore messages sent from a user logged in as the bot
if b.mc.User.Username == message.Username {
return true
}
// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
if message.Post.HasReactions {
return true
}
// ignore messages from other teams than ours
if message.Raw.Data["team_id"].(string) != b.TeamID {
return true
}
// only handle posted, edited or deleted events
if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED || message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) {
return true
}
return false
return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID)
}

View File

@ -0,0 +1,74 @@
package brocketchat
import (
"github.com/42wim/matterbridge/bridge/config"
)
func (b *Brocketchat) handleRocket() {
messages := make(chan *config.Message)
if b.GetString("WebhookBindAddress") != "" {
b.Log.Debugf("Choosing webhooks based receiving")
go b.handleRocketHook(messages)
} else {
b.Log.Debugf("Choosing login/password based receiving")
go b.handleRocketClient(messages)
}
for message := range messages {
message.Account = b.Account
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
b.Log.Debugf("<= Message is %#v", message)
b.Remote <- *message
}
}
func (b *Brocketchat) handleRocketHook(messages chan *config.Message) {
for {
message := b.rh.Receive()
b.Log.Debugf("Receiving from rockethook %#v", message)
// do not loop
if message.UserName == b.GetString("Nick") {
continue
}
messages <- &config.Message{
UserID: message.UserID,
Username: message.UserName,
Text: message.Text,
Channel: message.ChannelName,
}
}
}
func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
for message := range b.messageChan {
// skip messages with same ID, apparently messages get duplicated for an unknown reason
if _, ok := b.cache.Get(message.ID); ok {
continue
}
b.cache.Add(message.ID, true)
b.Log.Debugf("message %#v", message)
m := message
if b.skipMessage(&m) {
b.Log.Debugf("Skipped message: %#v", message)
continue
}
rmsg := &config.Message{Text: message.Msg,
Username: message.User.UserName,
Channel: b.getChannelName(message.RoomID),
Account: b.Account,
UserID: message.User.ID,
ID: message.ID,
}
messages <- rmsg
}
}
func (b *Brocketchat) handleUploadFile(msg *config.Message) error {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if err := b.uploadFile(&fi, b.getChannelID(msg.Channel)); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,198 @@
package brocketchat
import (
"context"
"io/ioutil"
"mime"
"net/http"
"net/url"
"strings"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
"github.com/matterbridge/Rocket.Chat.Go.SDK/realtime"
"github.com/matterbridge/Rocket.Chat.Go.SDK/rest"
"github.com/nelsonken/gomf"
)
func (b *Brocketchat) doConnectWebhookBind() error {
switch {
case b.GetString("WebhookURL") != "":
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true})
b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending)")
err := b.apiLogin()
if err != nil {
return err
}
default:
b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
}
return nil
}
func (b *Brocketchat) doConnectWebhookURL() error {
b.Log.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true})
if b.GetString("Login") != "" {
b.Log.Info("Connecting using login/password (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
}
return nil
}
func (b *Brocketchat) apiLogin() error {
b.Log.Debugf("handling apiLogin()")
credentials := &models.UserCredentials{Email: b.GetString("login"), Password: b.GetString("password")}
myURL, err := url.Parse(b.GetString("server"))
if err != nil {
return err
}
client, err := realtime.NewClient(myURL, b.GetBool("debug"))
b.c = client
if err != nil {
return err
}
restclient := rest.NewClient(myURL, b.GetBool("debug"))
user, err := b.c.Login(credentials)
if err != nil {
return err
}
b.user = user
b.r = restclient
err = b.r.Login(credentials)
if err != nil {
return err
}
b.Log.Info("Connection succeeded")
return nil
}
func (b *Brocketchat) getChannelName(id string) string {
b.RLock()
defer b.RUnlock()
if name, ok := b.channelMap[id]; ok {
return name
}
return ""
}
func (b *Brocketchat) getChannelID(name string) string {
b.RLock()
defer b.RUnlock()
for k, v := range b.channelMap {
if v == name {
return k
}
}
return ""
}
func (b *Brocketchat) skipMessage(message *models.Message) bool {
return message.User.ID == b.user.ID
}
func (b *Brocketchat) uploadFile(fi *config.FileInfo, channel string) error {
fb := gomf.New()
if err := fb.WriteField("description", fi.Comment); err != nil {
return err
}
sp := strings.Split(fi.Name, ".")
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
if !strings.Contains(mtype, "image") && !strings.Contains(mtype, "video") {
return nil
}
if err := fb.WriteFile("file", fi.Name, mtype, *fi.Data); err != nil {
return err
}
req, err := fb.GetHTTPRequest(context.TODO(), b.GetString("server")+"/api/v1/rooms.upload/"+channel)
if err != nil {
return err
}
req.Header.Add("X-Auth-Token", b.user.Token)
req.Header.Add("X-User-Id", b.user.ID)
client := &http.Client{
Timeout: time.Second * 5,
}
resp, err := client.Do(req)
if err != nil {
return err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != 200 {
b.Log.Errorf("failed: %#v", string(body))
}
return nil
}
// sendWebhook uses the configured WebhookURL to send the message
func (b *Brocketchat) sendWebhook(msg *config.Message) error {
// skip events
if msg.Event != "" {
return nil
}
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
if msg.Extra != nil {
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
for _, rmsg := range helper.HandleExtra(msg, b.General) {
rmsg := rmsg // scopelint
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Channel: rmsg.Channel,
UserName: rmsg.Username,
Text: rmsg.Text,
Props: make(map[string]interface{}),
}
if err := b.mh.Send(matterMessage); err != nil {
b.Log.Errorf("sendWebhook failed: %s ", err)
}
}
// webhook doesn't support file uploads, so we add the url manually
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += fi.URL
}
}
}
}
iconURL := config.GetIconURL(msg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Channel: msg.Channel,
UserName: msg.Username,
Text: msg.Text,
}
if msg.Avatar != "" {
matterMessage.IconURL = msg.Avatar
}
err := b.mh.Send(matterMessage)
if err != nil {
b.Log.Info(err)
return err
}
return nil
}

View File

@ -1,21 +1,47 @@
package brocketchat
import (
"errors"
"strings"
"sync"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook"
lru "github.com/hashicorp/golang-lru"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
"github.com/matterbridge/Rocket.Chat.Go.SDK/realtime"
"github.com/matterbridge/Rocket.Chat.Go.SDK/rest"
)
type Brocketchat struct {
mh *matterhook.Client
rh *rockethook.Client
mh *matterhook.Client
rh *rockethook.Client
c *realtime.Client
r *rest.Client
cache *lru.Cache
*bridge.Config
messageChan chan models.Message
channelMap map[string]string
user *models.User
sync.RWMutex
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Brocketchat{Config: cfg}
newCache, err := lru.New(100)
if err != nil {
cfg.Log.Fatalf("Could not create LRU cache for rocketchat bridge: %v", err)
}
b := &Brocketchat{
Config: cfg,
messageChan: make(chan models.Message),
channelMap: make(map[string]string),
cache: newCache,
}
b.Log.Debugf("enabling rocketchat")
return b
}
func (b *Brocketchat) Command(cmd string) string {
@ -23,70 +49,122 @@ func (b *Brocketchat) Command(cmd string) string {
}
func (b *Brocketchat) Connect() error {
b.Log.Info("Connecting webhooks")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true})
b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
go b.handleRocketHook()
if b.GetString("WebhookBindAddress") != "" {
if err := b.doConnectWebhookBind(); err != nil {
return err
}
go b.handleRocket()
return nil
}
switch {
case b.GetString("WebhookURL") != "":
if err := b.doConnectWebhookURL(); err != nil {
return err
}
go b.handleRocket()
return nil
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending and receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleRocket()
}
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" &&
b.GetString("Login") == "" {
return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Login/Password/Server configured")
}
return nil
}
func (b *Brocketchat) Disconnect() error {
return nil
}
func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
if b.c == nil {
return nil
}
id, err := b.c.GetChannelId(strings.TrimPrefix(channel.Name, "#"))
if err != nil {
return err
}
b.Lock()
b.channelMap[id] = channel.Name
b.Unlock()
mychannel := &models.Channel{ID: id, Name: strings.TrimPrefix(channel.Name, "#")}
if err := b.c.JoinChannel(id); err != nil {
return err
}
if err := b.c.SubscribeToMessageStream(mychannel, b.messageChan); err != nil {
return err
}
return nil
}
func (b *Brocketchat) Send(msg config.Message) (string, error) {
// ignore delete messages
// strip the # if people has set this
msg.Channel = strings.TrimPrefix(msg.Channel, "#")
channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel}
// Delete message
if msg.Event == config.EventMsgDelete {
return "", nil
if msg.ID == "" {
return "", nil
}
return msg.ID, b.c.DeleteMessage(&models.Message{ID: msg.ID})
}
b.Log.Debugf("=> Receiving %#v", msg)
// Use webhook to send the message
if b.GetString("WebhookURL") != "" {
return "", b.sendWebhook(&msg)
}
// Prepend nick if configured
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
// Edit message if we have an ID
if msg.ID != "" {
return msg.ID, b.c.EditMessage(&models.Message{ID: msg.ID, Msg: msg.Text, RoomID: b.getChannelID(msg.Channel)})
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
rmsg := rmsg // scopelint
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{IconURL: 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
}
// strip the # if people has set this
rmsg.Channel = strings.TrimPrefix(rmsg.Channel, "#")
smsg := &models.Message{
RoomID: b.getChannelID(rmsg.Channel),
Msg: rmsg.Username + rmsg.Text,
PostMessage: models.PostMessage{
Avatar: rmsg.Avatar,
Alias: rmsg.Username,
},
}
if _, err := b.c.SendMessage(smsg); err != nil {
b.Log.Errorf("SendMessage failed: %s", err)
}
}
if len(msg.Extra["file"]) > 0 {
return "", b.handleUploadFile(&msg)
}
}
iconURL := config.GetIconURL(&msg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{IconURL: iconURL}
matterMessage.Channel = msg.Channel
matterMessage.UserName = msg.Username
matterMessage.Type = ""
matterMessage.Text = msg.Text
err := b.mh.Send(matterMessage)
if err != nil {
b.Log.Info(err)
smsg := &models.Message{
RoomID: channel.ID,
Msg: msg.Text,
PostMessage: models.PostMessage{
Avatar: msg.Avatar,
Alias: msg.Username,
},
}
rmsg, err := b.c.SendMessage(smsg)
if rmsg == nil {
return "", err
}
return "", nil
}
func (b *Brocketchat) handleRocketHook() {
for {
message := b.rh.Receive()
b.Log.Debugf("Receiving from rockethook %#v", message)
// do not loop
if message.UserName == b.GetString("Nick") {
continue
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.UserName, b.Account)
b.Remote <- config.Message{Text: message.Text, Username: message.UserName, Channel: message.ChannelName, Account: b.Account, UserID: message.UserID}
}
return rmsg.ID, err
}

View File

@ -34,7 +34,7 @@ func (b *Bslack) handleSlack() {
message.Text = html.UnescapeString(message.Text)
// Add the avatar
message.Avatar = b.getAvatar(message.UserID)
message.Avatar = b.users.getAvatar(message.UserID)
b.Log.Debugf("<= Message is %#v", message)
b.Remote <- *message
@ -75,21 +75,21 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
// When we join a channel we update the full list of users as
// well as the information for the channel that we joined as this
// should now tell that we are a member of it.
b.populateUsers()
b.channelsMutex.Lock()
b.channelsByID[ev.Channel.ID] = &ev.Channel
b.channelsByName[ev.Channel.Name] = &ev.Channel
b.channelsMutex.Unlock()
b.channels.registerChannel(ev.Channel)
case *slack.ConnectedEvent:
b.si = ev.Info
b.populateChannels()
b.populateUsers()
b.channels.populateChannels(true)
b.users.populateUsers(true)
case *slack.InvalidAuthEvent:
b.Log.Fatalf("Invalid Token %#v", ev)
case *slack.ConnectionErrorEvent:
b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
case *slack.MemberJoinedChannelEvent:
b.users.populateUser(ev.User)
case *slack.LatencyReport:
continue
default:
b.Log.Debugf("Unhandled incoming event: %T", ev)
}
}
}
@ -116,6 +116,11 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
return b.GetBool(noSendJoinConfig)
case sPinnedItem, sUnpinnedItem:
return true
case sChannelTopic, sChannelPurpose:
// Skip the event if our bot/user account changed the topic/purpose
if ev.User == b.si.User.ID {
return true
}
}
// Skip any messages that we made ourselves or from 'slackbot' (see #527).
@ -136,7 +141,6 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
if len(ev.Files) > 0 {
return b.filesCached(ev.Files)
}
return false
}
@ -185,6 +189,9 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
// This is probably a webhook we couldn't resolve.
return nil, fmt.Errorf("message handling resulted in an empty bot message (probably an incoming webhook we couldn't resolve): %#v", ev)
}
if ev.SubMessage != nil {
return nil, fmt.Errorf("message handling resulted in an empty message: %#v with submessage %#v", ev, ev.SubMessage)
}
return nil, fmt.Errorf("message handling resulted in an empty message: %#v", ev)
}
return rmsg, nil
@ -193,7 +200,6 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
switch ev.SubType {
case sChannelJoined, sMemberJoined:
b.populateUsers()
// There's no further processing needed on channel events
// so we return 'true'.
return true
@ -201,7 +207,15 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
rmsg.Username = sSystemUser
rmsg.Event = config.EventJoinLeave
case sChannelTopic, sChannelPurpose:
b.channels.populateChannels(false)
rmsg.Event = config.EventTopicChange
case sMessageChanged:
rmsg.Text = ev.SubMessage.Text
// handle deleted thread starting messages
if ev.SubMessage.Text == "This message was deleted." {
rmsg.Event = config.EventMsgDelete
return true
}
case sMessageDeleted:
rmsg.Text = config.EventMsgDelete
rmsg.Event = config.EventMsgDelete
@ -242,14 +256,14 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
for i := range ev.Files {
if err := b.handleDownloadFile(rmsg, &ev.Files[i]); err != nil {
if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
b.Log.Errorf("Could not download incoming file: %#v", err)
}
}
}
func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) {
channelInfo, err := b.getChannelByID(ev.Channel)
channelInfo, err := b.channels.getChannelByID(ev.Channel)
if err != nil {
return nil, err
}
@ -261,7 +275,7 @@ func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message,
}
// handleDownloadFile handles file download
func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File) error {
func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retry bool) error {
if b.fileCached(file) {
return nil
}
@ -277,6 +291,12 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File) erro
return fmt.Errorf("download %s failed %#v", file.URLPrivateDownload, err)
}
if len(*data) != file.Size && !retry {
b.Log.Debugf("Data size (%d) is not equal to size declared (%d)\n", len(*data), file.Size)
time.Sleep(1 * time.Second)
return b.handleDownloadFile(rmsg, file, true)
}
// If a comment is attached to the file(s) it is in the 'Text' field of the Slack messge event
// and should be added as comment to only one of the files. We reset the 'Text' field to ensure
// that the comment is not duplicated.
@ -286,6 +306,29 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File) erro
return nil
}
// handleGetChannelMembers handles messages containing the GetChannelMembers event
// Sends a message to the router containing *config.ChannelMembers
func (b *Bslack) handleGetChannelMembers(rmsg *config.Message) bool {
if rmsg.Event != config.EventGetChannelMembers {
return false
}
cMembers := b.channels.getChannelMembers(b.users)
extra := make(map[string][]interface{})
extra[config.EventGetChannelMembers] = append(extra[config.EventGetChannelMembers], cMembers)
msg := config.Message{
Extra: extra,
Event: config.EventGetChannelMembers,
Account: b.Account,
}
b.Log.Debugf("sending msg to remote %#v", msg)
b.Remote <- msg
return true
}
// fileCached implements Matterbridge's caching logic for files
// shared via Slack.
//

View File

@ -1,7 +1,6 @@
package bslack
import (
"context"
"fmt"
"regexp"
"strings"
@ -9,162 +8,14 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/nlopes/slack"
"github.com/sirupsen/logrus"
)
func (b *Bslack) getUser(id string) *slack.User {
b.usersMutex.RLock()
defer b.usersMutex.RUnlock()
return b.users[id]
}
func (b *Bslack) getUsername(id string) string {
if user := b.getUser(id); user != nil {
if user.Profile.DisplayName != "" {
return user.Profile.DisplayName
}
return user.Name
}
b.Log.Warnf("Could not find user with ID '%s'", id)
return ""
}
func (b *Bslack) getAvatar(id string) string {
if user := b.getUser(id); user != nil {
return user.Profile.Image48
}
return ""
}
func (b *Bslack) getChannel(channel string) (*slack.Channel, error) {
if strings.HasPrefix(channel, "ID:") {
return b.getChannelByID(strings.TrimPrefix(channel, "ID:"))
}
return b.getChannelByName(channel)
}
func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
if channel, ok := b.channelsByName[name]; ok {
return channel, nil
}
return nil, fmt.Errorf("%s: channel %s not found", b.Account, name)
}
func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
if channel, ok := b.channelsByID[ID]; ok {
return channel, nil
}
return nil, fmt.Errorf("%s: channel %s not found", b.Account, ID)
}
const minimumRefreshInterval = 10 * time.Second
func (b *Bslack) populateUsers() {
b.refreshMutex.Lock()
if time.Now().Before(b.earliestUserRefresh) || b.refreshInProgress {
b.Log.Debugf("Not refreshing user list as it was done less than %v ago.",
minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newUsers := map[string]*slack.User{}
pagination := b.sc.GetUsersPaginated(slack.GetUsersOptionLimit(200))
for {
var err error
pagination, err = pagination.Next(context.Background())
if err != nil {
if pagination.Done(err) {
break
}
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve users: %#v", err)
return
}
continue
}
for i := range pagination.Users {
newUsers[pagination.Users[i].ID] = &pagination.Users[i]
}
}
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
b.users = newUsers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestUserRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
func (b *Bslack) populateChannels() {
b.refreshMutex.Lock()
if time.Now().Before(b.earliestChannelRefresh) || b.refreshInProgress {
b.Log.Debugf("Not refreshing channel list as it was done less than %v seconds ago.",
minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newChannelsByID := map[string]*slack.Channel{}
newChannelsByName := map[string]*slack.Channel{}
// We only retrieve public and private channels, not IMs
// and MPIMs as those do not have a channel name.
queryParams := &slack.GetConversationsParameters{
ExcludeArchived: "true",
Types: []string{"public_channel,private_channel"},
}
for {
channels, nextCursor, err := b.sc.GetConversations(queryParams)
if err != nil {
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve channels: %#v", err)
return
}
continue
}
for i := range channels {
newChannelsByID[channels[i].ID] = &channels[i]
newChannelsByName[channels[i].Name] = &channels[i]
}
if nextCursor == "" {
break
}
queryParams.Cursor = nextCursor
}
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelsByID = newChannelsByID
b.channelsByName = newChannelsByName
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestChannelRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
// router before we apply message-dependent modifications.
func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) {
// Use our own func because rtm.GetChannelInfo doesn't work for private channels.
channel, err := b.getChannelByID(ev.Channel)
channel, err := b.channels.getChannelByID(ev.Channel)
if err != nil {
return nil, err
}
@ -191,6 +42,13 @@ func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Messag
}
}
// For edits, only submessage has thread ts.
// Ensures edits to threaded messages maintain their prefix hint on the
// unthreaded end.
if ev.SubMessage != nil {
rmsg.ParentID = ev.SubMessage.ThreadTimestamp
}
if err = b.populateMessageWithUserInfo(ev, rmsg); err != nil {
return nil, err
}
@ -219,7 +77,7 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi
return nil
}
user := b.getUser(userID)
user := b.users.getUser(userID)
if user == nil {
return fmt.Errorf("could not find information for user with id %s", ev.User)
}
@ -245,13 +103,14 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
break
}
if err = b.handleRateLimit(err); err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Could not retrieve bot information: %#v", err)
return err
}
}
b.Log.Debugf("Found bot %#v", bot)
if bot.Name != "" && bot.Name != "Slack API Tester" {
if bot.Name != "" {
rmsg.Username = bot.Name
if ev.Username != "" {
rmsg.Username = ev.Username
@ -262,17 +121,34 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
}
var (
mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`)
mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`)
codeFenceRE = regexp.MustCompile(`(?m)^` + "```" + `\w+$`)
topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`)
)
func (b *Bslack) extractTopicOrPurpose(text string) (string, string) {
r := topicOrPurposeRE.FindStringSubmatch(text)
if len(r) == 5 {
action, updateType, extracted := r[2], r[3], r[4]
switch action {
case "set":
return updateType, extracted
case "cleared":
return updateType, ""
}
}
b.Log.Warnf("Encountered channel topic or purpose change message with unexpected format: %s", text)
return "unknown", ""
}
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (b *Bslack) replaceMention(text string) string {
replaceFunc := func(match string) string {
userID := strings.Trim(match, "@<>")
if username := b.getUsername(userID); userID != "" {
if username := b.users.getUsername(userID); userID != "" {
return "@" + username
}
return match
@ -312,12 +188,42 @@ func (b *Bslack) replaceURL(text string) string {
return text
}
func (b *Bslack) handleRateLimit(err error) error {
func (b *Bslack) replaceCodeFence(text string) string {
return codeFenceRE.ReplaceAllString(text, "```")
}
// getUsersInConversation returns an array of userIDs that are members of channelID
func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
channelMembers := []string{}
for {
queryParams := &slack.GetUsersInConversationParameters{
ChannelID: channelID,
}
members, nextCursor, err := b.sc.GetUsersInConversation(queryParams)
if err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
return channelMembers, fmt.Errorf("Could not retrieve users in channels: %#v", err)
}
continue
}
channelMembers = append(channelMembers, members...)
if nextCursor == "" {
break
}
queryParams.Cursor = nextCursor
}
return channelMembers, nil
}
func handleRateLimit(log *logrus.Entry, err error) error {
rateLimit, ok := err.(*slack.RateLimitedError)
if !ok {
return err
}
b.Log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
time.Sleep(rateLimit.RetryAfter)
return nil
}

View File

@ -0,0 +1,36 @@
package bslack
import (
"io/ioutil"
"testing"
"github.com/42wim/matterbridge/bridge"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestExtractTopicOrPurpose(t *testing.T) {
testcases := map[string]struct {
input string
wantChangeType string
wantOutput string
}{
"success - topic type": {"@someone set channel topic: foo bar", "topic", "foo bar"},
"success - purpose type": {"@someone set channel purpose: foo bar", "purpose", "foo bar"},
"success - one line": {"@someone set channel topic: foo bar", "topic", "foo bar"},
"success - multi-line": {"@someone set channel topic: foo\nbar", "topic", "foo\nbar"},
"success - cleared": {"@someone cleared channel topic", "topic", ""},
"error - unhandled": {"some unmatched message", "unknown", ""},
}
logger := logrus.New()
logger.SetOutput(ioutil.Discard)
cfg := &bridge.Config{Bridge: &bridge.Bridge{Log: logrus.NewEntry(logger)}}
b := newBridge(cfg)
for name, tc := range testcases {
gotChangeType, gotOutput := b.extractTopicOrPurpose(tc.input)
assert.Equalf(t, tc.wantChangeType, gotChangeType, "This testcase failed: %s", name)
assert.Equalf(t, tc.wantOutput, gotOutput, "This testcase failed: %s", name)
}
}

View File

@ -55,14 +55,18 @@ func (b *BLegacy) Connect() error {
})
if b.GetString(tokenConfig) != "" {
b.Log.Info("Connecting using token (receiving)")
b.sc = slack.New(b.GetString(tokenConfig))
b.sc = slack.New(b.GetString(tokenConfig), slack.OptionDebug(b.GetBool("debug")))
b.channels = newChannelManager(b.Log, b.sc)
b.users = newUserManager(b.Log, b.sc)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
}
} else if b.GetString(tokenConfig) != "" {
b.Log.Info("Connecting using token (sending and receiving)")
b.sc = slack.New(b.GetString(tokenConfig))
b.sc = slack.New(b.GetString(tokenConfig), slack.OptionDebug(b.GetBool("debug")))
b.channels = newChannelManager(b.Log, b.sc)
b.users = newUserManager(b.Log, b.sc)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()

View File

@ -30,17 +30,8 @@ type Bslack struct {
uuid string
useChannelID bool
users map[string]*slack.User
usersMutex sync.RWMutex
channelsByID map[string]*slack.Channel
channelsByName map[string]*slack.Channel
channelsMutex sync.RWMutex
refreshInProgress bool
earliestChannelRefresh time.Time
earliestUserRefresh time.Time
refreshMutex sync.Mutex
channels *channels
users *users
}
const (
@ -48,6 +39,7 @@ const (
sChannelLeave = "channel_leave"
sChannelJoined = "channel_joined"
sMemberJoined = "member_joined_channel"
sMessageChanged = "message_changed"
sMessageDeleted = "message_deleted"
sSlackAttachment = "slack_attachment"
sPinnedItem = "pinned_item"
@ -76,14 +68,9 @@ func New(cfg *bridge.Config) bridge.Bridger {
// Print a deprecation warning for legacy non-bot tokens (#527).
token := cfg.GetString(tokenConfig)
if token != "" && !strings.HasPrefix(token, "xoxb") {
cfg.Log.Error("Non-bot token detected. It is STRONGLY recommended to use a proper bot-token instead.")
cfg.Log.Error("Legacy tokens may be deprecated by Slack at short notice. See the Matterbridge GitHub wiki for a migration guide.")
cfg.Log.Error("See https://github.com/42wim/matterbridge/wiki/Slack-bot-setup")
cfg.Log.Error("")
cfg.Log.Error("To continue using a legacy token please move your configuration to a \"slack-legacy\" bridge instead.")
cfg.Log.Error("See https://github.com/42wim/matterbridge/wiki/Section-Slack-(basic)#legacy-configuration)")
cfg.Log.Error("Delaying start of bridge by 30 seconds. Future Matterbridge release will fail here unless you use a \"slack-legacy\" bridge.")
time.Sleep(30 * time.Second)
cfg.Log.Warn("Non-bot token detected. It is STRONGLY recommended to use a proper bot-token instead.")
cfg.Log.Warn("Legacy tokens may be deprecated by Slack at short notice. See the Matterbridge GitHub wiki for a migration guide.")
cfg.Log.Warn("See https://github.com/42wim/matterbridge/wiki/Slack-bot-setup")
return NewLegacy(cfg)
}
return newBridge(cfg)
@ -95,14 +82,9 @@ func newBridge(cfg *bridge.Config) *Bslack {
cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err)
}
b := &Bslack{
Config: cfg,
uuid: xid.New().String(),
cache: newCache,
users: map[string]*slack.User{},
channelsByID: map[string]*slack.Channel{},
channelsByName: map[string]*slack.Channel{},
earliestChannelRefresh: time.Now(),
earliestUserRefresh: time.Now(),
Config: cfg,
uuid: xid.New().String(),
cache: newCache,
}
return b
}
@ -122,7 +104,12 @@ func (b *Bslack) Connect() error {
// If we have a token we use the Slack websocket-based RTM for both sending and receiving.
if token := b.GetString(tokenConfig); token != "" {
b.Log.Info("Connecting using token")
b.sc = slack.New(token)
b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug")))
b.channels = newChannelManager(b.Log, b.sc)
b.users = newUserManager(b.Log, b.sc)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
@ -164,9 +151,9 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
return nil
}
b.populateChannels()
b.channels.populateChannels(false)
channelInfo, err := b.getChannel(channel.Name)
channelInfo, err := b.channels.getChannel(channel.Name)
if err != nil {
return fmt.Errorf("could not join channel: %#v", err)
}
@ -192,6 +179,8 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
}
msg.Text = b.replaceCodeFence(msg.Text)
// Make a action /me of the message
if msg.Event == config.EventUserAction {
msg.Text = "_" + msg.Text + "_"
@ -199,16 +188,16 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
// Use webhook to send the message
if b.GetString(outgoingWebhookConfig) != "" {
return b.sendWebhook(msg)
return "", b.sendWebhook(msg)
}
return b.sendRTM(msg)
}
// sendWebhook uses the configured WebhookURL to send the message
func (b *Bslack) sendWebhook(msg config.Message) (string, error) {
func (b *Bslack) sendWebhook(msg config.Message) error {
// Skip events.
if msg.Event != "" {
return "", nil
return nil
}
if b.GetBool(useNickPrefixConfig) {
@ -263,13 +252,18 @@ func (b *Bslack) sendWebhook(msg config.Message) (string, error) {
}
if err := b.mh.Send(matterMessage); err != nil {
b.Log.Errorf("Failed to send message via webhook: %#v", err)
return "", err
return err
}
return "", nil
return nil
}
func (b *Bslack) sendRTM(msg config.Message) (string, error) {
channelInfo, err := b.getChannel(msg.Channel)
// Handle channelmember messages.
if handled := b.handleGetChannelMembers(&msg); handled {
return "", nil
}
channelInfo, err := b.channels.getChannel(msg.Channel)
if err != nil {
return "", fmt.Errorf("could not send message: %v", err)
}
@ -280,8 +274,20 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
return "", nil
}
// Handle message deletions.
var handled bool
// Handle topic/purpose updates.
if handled, err = b.handleTopicOrPurpose(&msg, channelInfo); handled {
return "", err
}
// Handle prefix hint for unthreaded messages.
if msg.ParentID == "msg-parent-not-found" {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
// Handle message deletions.
if handled, err = b.deleteMessage(&msg, channelInfo); handled {
return msg.ID, err
}
@ -296,12 +302,13 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
return msg.ID, err
}
messageParameters := b.prepareMessageParameters(&msg)
// Upload a file if it exists.
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
_, _, err = b.rtm.PostMessage(channelInfo.ID, rmsg.Username+rmsg.Text, *messageParameters)
extraMsgs := helper.HandleExtra(&msg, b.General)
for i := range extraMsgs {
rmsg := &extraMsgs[i]
rmsg.Text = rmsg.Username + rmsg.Text
_, err = b.postMessage(rmsg, channelInfo)
if err != nil {
b.Log.Error(err)
}
@ -311,7 +318,50 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
}
// Post message.
return b.postMessage(&msg, messageParameters, channelInfo)
return b.postMessage(&msg, channelInfo)
}
func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) error {
var updateFunc func(channelID string, value string) (*slack.Channel, error)
incomingChangeType, text := b.extractTopicOrPurpose(msg.Text)
switch incomingChangeType {
case "topic":
updateFunc = b.rtm.SetTopicOfConversation
case "purpose":
updateFunc = b.rtm.SetPurposeOfConversation
default:
b.Log.Errorf("Unhandled type received from extractTopicOrPurpose: %s", incomingChangeType)
return nil
}
for {
_, err := updateFunc(channelInfo.ID, text)
if err == nil {
return nil
}
if err = handleRateLimit(b.Log, err); err != nil {
return err
}
}
}
// handles updating topic/purpose and determining whether to further propagate update messages.
func (b *Bslack) handleTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
if msg.Event != config.EventTopicChange {
return false, nil
}
if b.GetBool("SyncTopic") {
return true, b.updateTopicOrPurpose(msg, channelInfo)
}
// Pass along to normal message handlers.
if b.GetBool("ShowTopicChange") {
return false, nil
}
// Swallow message as handled no-op.
return true, nil
}
func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
@ -330,7 +380,7 @@ func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel)
return true, nil
}
if err = b.handleRateLimit(err); err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Failed to delete user message from Slack: %#v", err)
return true, err
}
@ -341,28 +391,39 @@ func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (b
if msg.ID == "" {
return false, nil
}
messageOptions := b.prepareMessageOptions(msg)
for {
_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, msg.Text)
messageOptions = append(messageOptions, slack.MsgOptionText(msg.Text, false))
_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...)
if err == nil {
return true, nil
}
if err = b.handleRateLimit(err); err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Failed to edit user message on Slack: %#v", err)
return true, err
}
}
}
func (b *Bslack) postMessage(msg *config.Message, messageParameters *slack.PostMessageParameters, channelInfo *slack.Channel) (string, error) {
func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (string, error) {
// don't post empty messages
if msg.Text == "" {
return "", nil
}
messageOptions := b.prepareMessageOptions(msg)
messageOptions = append(
messageOptions,
slack.MsgOptionText(msg.Text, false),
slack.MsgOptionEnableLinkUnfurl(),
)
for {
_, id, err := b.rtm.PostMessage(channelInfo.ID, msg.Text, *messageParameters)
_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
if err == nil {
return id, nil
}
if err = b.handleRateLimit(err); err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Failed to sent user message to Slack: %#v", err)
return "", err
}
@ -385,11 +446,16 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
ts := time.Now()
b.Log.Debugf("Adding file %s to cache at %s with timestamp", fi.Name, ts.String())
b.cache.Add("filename"+fi.Name, ts)
initialComment := fmt.Sprintf("File from %s", msg.Username)
if fi.Comment != "" {
initialComment += fmt.Sprintf("with comment: %s", fi.Comment)
}
res, err := b.sc.UploadFile(slack.FileUploadParameters{
Reader: bytes.NewReader(*fi.Data),
Filename: fi.Name,
Channels: []string{channelID},
InitialComment: fi.Comment,
Reader: bytes.NewReader(*fi.Data),
Filename: fi.Name,
Channels: []string{channelID},
InitialComment: initialComment,
ThreadTimestamp: msg.ParentID,
})
if err != nil {
b.Log.Errorf("uploadfile %#v", err)
@ -402,7 +468,7 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
}
}
func (b *Bslack) prepareMessageParameters(msg *config.Message) *slack.PostMessageParameters {
func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
params := slack.NewPostMessageParameters()
if b.GetBool(useNickPrefixConfig) {
params.AsUser = true
@ -414,17 +480,23 @@ func (b *Bslack) prepareMessageParameters(msg *config.Message) *slack.PostMessag
if msg.Avatar != "" {
params.IconURL = msg.Avatar
}
var attachments []slack.Attachment
// add a callback ID so we can see we created it
params.Attachments = append(params.Attachments, slack.Attachment{CallbackID: "matterbridge_" + b.uuid})
attachments = append(attachments, slack.Attachment{CallbackID: "matterbridge_" + b.uuid})
// add file attachments
params.Attachments = append(params.Attachments, b.createAttach(msg.Extra)...)
attachments = append(attachments, b.createAttach(msg.Extra)...)
// add slack attachments (from another slack bridge)
if msg.Extra != nil {
for _, attach := range msg.Extra[sSlackAttachment] {
params.Attachments = append(params.Attachments, attach.([]slack.Attachment)...)
attachments = append(attachments, attach.([]slack.Attachment)...)
}
}
return &params
var opts []slack.MsgOption
opts = append(opts, slack.MsgOptionAttachments(attachments...))
opts = append(opts, slack.MsgOptionPostMessageParameters(params))
return opts
}
func (b *Bslack) createAttach(extra map[string][]interface{}) []slack.Attachment {

View File

@ -0,0 +1,335 @@
package bslack
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/nlopes/slack"
"github.com/sirupsen/logrus"
)
const minimumRefreshInterval = 10 * time.Second
type users struct {
log *logrus.Entry
sc *slack.Client
users map[string]*slack.User
usersMutex sync.RWMutex
usersSyncPoints map[string]chan struct{}
refreshInProgress bool
earliestRefresh time.Time
refreshMutex sync.Mutex
}
func newUserManager(log *logrus.Entry, sc *slack.Client) *users {
return &users{
log: log,
sc: sc,
users: make(map[string]*slack.User),
usersSyncPoints: make(map[string]chan struct{}),
earliestRefresh: time.Now(),
}
}
func (b *users) getUser(id string) *slack.User {
b.usersMutex.RLock()
user, ok := b.users[id]
b.usersMutex.RUnlock()
if ok {
return user
}
b.populateUser(id)
b.usersMutex.RLock()
defer b.usersMutex.RUnlock()
return b.users[id]
}
func (b *users) getUsername(id string) string {
if user := b.getUser(id); user != nil {
if user.Profile.DisplayName != "" {
return user.Profile.DisplayName
}
return user.Name
}
b.log.Warnf("Could not find user with ID '%s'", id)
return ""
}
func (b *users) getAvatar(id string) string {
if user := b.getUser(id); user != nil {
return user.Profile.Image48
}
return ""
}
func (b *users) populateUser(userID string) {
for {
b.usersMutex.Lock()
_, exists := b.users[userID]
if exists {
// already in cache
b.usersMutex.Unlock()
return
}
if syncPoint, ok := b.usersSyncPoints[userID]; ok {
// Another goroutine is already populating this user for us so wait on it to finish.
b.usersMutex.Unlock()
<-syncPoint
// We do not return and iterate again to check that the entry does indeed exist
// in case the previous query failed for some reason.
} else {
b.usersSyncPoints[userID] = make(chan struct{})
break
}
}
// Do not hold the lock while fetching information from Slack
// as this might take an unbounded amount of time.
b.usersMutex.Unlock()
user, err := b.sc.GetUserInfo(userID)
if err != nil {
b.log.Debugf("GetUserInfo failed for %v: %v", userID, err)
return
}
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
// Register user information.
b.users[userID] = user
// Wake up any waiting goroutines and remove the synchronization point.
close(b.usersSyncPoints[userID])
delete(b.usersSyncPoints, userID)
}
func (b *users) populateUsers(wait bool) {
b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
b.log.Debugf("Not refreshing user list as it was done less than %v ago.", minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
for b.refreshInProgress {
b.refreshMutex.Unlock()
time.Sleep(time.Second)
b.refreshMutex.Lock()
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newUsers := map[string]*slack.User{}
pagination := b.sc.GetUsersPaginated(slack.GetUsersOptionLimit(200))
count := 0
for {
var err error
pagination, err = pagination.Next(context.Background())
time.Sleep(time.Second)
if err != nil {
if pagination.Done(err) {
break
}
if err = handleRateLimit(b.log, err); err != nil {
b.log.Errorf("Could not retrieve users: %#v", err)
return
}
continue
}
for i := range pagination.Users {
newUsers[pagination.Users[i].ID] = &pagination.Users[i]
}
b.log.Debugf("getting %d users", len(pagination.Users))
count++
// more > 2000 users, slack will complain and ratelimit. break
if count > 10 {
b.log.Info("Large slack detected > 2000 users, skipping loading complete userlist.")
break
}
}
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
b.users = newUsers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
type channels struct {
log *logrus.Entry
sc *slack.Client
channelsByID map[string]*slack.Channel
channelsByName map[string]*slack.Channel
channelsMutex sync.RWMutex
channelMembers map[string][]string
channelMembersMutex sync.RWMutex
refreshInProgress bool
earliestRefresh time.Time
refreshMutex sync.Mutex
}
func newChannelManager(log *logrus.Entry, sc *slack.Client) *channels {
return &channels{
log: log,
sc: sc,
channelsByID: make(map[string]*slack.Channel),
channelsByName: make(map[string]*slack.Channel),
earliestRefresh: time.Now(),
}
}
func (b *channels) getChannel(channel string) (*slack.Channel, error) {
if strings.HasPrefix(channel, "ID:") {
return b.getChannelByID(strings.TrimPrefix(channel, "ID:"))
}
return b.getChannelByName(channel)
}
func (b *channels) getChannelByName(name string) (*slack.Channel, error) {
return b.getChannelBy(name, b.channelsByName)
}
func (b *channels) getChannelByID(id string) (*slack.Channel, error) {
return b.getChannelBy(id, b.channelsByID)
}
func (b *channels) getChannelBy(lookupKey string, lookupMap map[string]*slack.Channel) (*slack.Channel, error) {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
if channel, ok := lookupMap[lookupKey]; ok {
return channel, nil
}
return nil, fmt.Errorf("channel %s not found", lookupKey)
}
func (b *channels) getChannelMembers(users *users) config.ChannelMembers {
b.channelMembersMutex.RLock()
defer b.channelMembersMutex.RUnlock()
membersInfo := config.ChannelMembers{}
for channelID, members := range b.channelMembers {
for _, member := range members {
channelName := ""
userName := ""
userNick := ""
user := users.getUser(member)
if user != nil {
userName = user.Name
userNick = user.Profile.DisplayName
}
channel, _ := b.getChannelByID(channelID)
if channel != nil {
channelName = channel.Name
}
memberInfo := config.ChannelMember{
Username: userName,
Nick: userNick,
UserID: member,
ChannelID: channelID,
ChannelName: channelName,
}
membersInfo = append(membersInfo, memberInfo)
}
}
return membersInfo
}
func (b *channels) registerChannel(channel slack.Channel) {
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelsByID[channel.ID] = &channel
b.channelsByName[channel.Name] = &channel
}
func (b *channels) populateChannels(wait bool) {
b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
b.log.Debugf("Not refreshing channel list as it was done less than %v seconds ago.", minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
for b.refreshInProgress {
b.refreshMutex.Unlock()
time.Sleep(time.Second)
b.refreshMutex.Lock()
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newChannelsByID := map[string]*slack.Channel{}
newChannelsByName := map[string]*slack.Channel{}
newChannelMembers := make(map[string][]string)
// We only retrieve public and private channels, not IMs
// and MPIMs as those do not have a channel name.
queryParams := &slack.GetConversationsParameters{
ExcludeArchived: "true",
Types: []string{"public_channel,private_channel"},
}
for {
channels, nextCursor, err := b.sc.GetConversations(queryParams)
if err != nil {
if err = handleRateLimit(b.log, err); err != nil {
b.log.Errorf("Could not retrieve channels: %#v", err)
return
}
continue
}
for i := range channels {
newChannelsByID[channels[i].ID] = &channels[i]
newChannelsByName[channels[i].Name] = &channels[i]
// also find all the members in every channel
// comment for now, issues on big slacks
/*
members, err := b.getUsersInConversation(channels[i].ID)
if err != nil {
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve channel members: %#v", err)
return
}
continue
}
newChannelMembers[channels[i].ID] = members
*/
}
if nextCursor == "" {
break
}
queryParams.Cursor = nextCursor
}
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelsByID = newChannelsByID
b.channelsByName = newChannelsByName
b.channelMembersMutex.Lock()
defer b.channelMembersMutex.Unlock()
b.channelMembers = newChannelMembers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}

View File

@ -9,7 +9,6 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/shazow/ssh-chat/sshd"
log "github.com/sirupsen/logrus"
)
type Bsshchat struct {
@ -23,21 +22,35 @@ func New(cfg *bridge.Config) bridge.Bridger {
}
func (b *Bsshchat) Connect() error {
var err error
b.Log.Infof("Connecting %s", b.GetString("Server"))
// connHandler will be called by 'sshd.ConnectShell()' below
// once the connection is established in order to handle it.
connErr := make(chan error, 1) // Needs to be buffered.
connSignal := make(chan struct{})
connHandler := func(r io.Reader, w io.WriteCloser) error {
b.r = bufio.NewScanner(r)
b.r.Scan()
b.w = w
if _, err := b.w.Write([]byte("/theme mono\r\n/quiet\r\n")); err != nil {
return err
}
close(connSignal) // Connection is established so we can signal the success.
return b.handleSSHChat()
}
go func() {
err = sshd.ConnectShell(b.GetString("Server"), b.GetString("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
})
// As a successful connection will result in this returning after the Connection
// method has already returned point we NEED to have a buffered channel to still
// be able to write.
connErr <- sshd.ConnectShell(b.GetString("Server"), b.GetString("Nick"), connHandler)
}()
if err != nil {
b.Log.Debugf("%#v", err)
select {
case err := <-connErr:
b.Log.Error("Connection failed")
return err
case <-connSignal:
}
b.Log.Info("Connection succeeded")
return nil
@ -59,27 +72,16 @@ func (b *Bsshchat) Send(msg config.Message) (string, error) {
b.Log.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 _, err := b.w.Write([]byte(rmsg.Username + rmsg.Text + "\r\n")); err != nil {
b.Log.Errorf("Could not send extra message: %#v", err)
}
}
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
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
b.w.Write([]byte(msg.Username + msg.Text))
}
return "", nil
return b.handleUploadFile(&msg)
}
}
b.w.Write([]byte(msg.Username + msg.Text + "\r\n"))
return "", nil
_, err := b.w.Write([]byte(msg.Username + msg.Text + "\r\n"))
return "", err
}
/*
@ -125,17 +127,39 @@ func (b *Bsshchat) handleSSHChat() error {
if !strings.Contains(b.r.Text(), "\033[K") {
continue
}
if strings.Contains(b.r.Text(), "Rate limiting is in effect") {
continue
}
res := strings.Split(stripPrompt(b.r.Text()), ":")
if res[0] == "-> Set theme" {
wait = false
log.Debugf("mono found, allowing")
b.Log.Debugf("mono found, allowing")
continue
}
if !wait {
b.Log.Debugf("<= Message %#v", res)
rmsg := config.Message{Username: res[0], Text: strings.Join(res[1:], ":"), Channel: "sshchat", Account: b.Account, UserID: "nick"}
rmsg := config.Message{Username: res[0], Text: strings.TrimSpace(strings.Join(res[1:], ":")), Channel: "sshchat", Account: b.Account, UserID: "nick"}
b.Remote <- rmsg
}
}
}
}
func (b *Bsshchat) handleUploadFile(msg *config.Message) (string, error) {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
if _, err := b.w.Write([]byte(msg.Username + msg.Text + "\r\n")); err != nil {
b.Log.Errorf("Could not send file message: %#v", err)
}
}
return "", nil
}

126
bridge/steam/handlers.go Normal file
View File

@ -0,0 +1,126 @@
package bsteam
import (
"fmt"
"strconv"
"github.com/42wim/matterbridge/bridge/config"
"github.com/Philipp15b/go-steam"
"github.com/Philipp15b/go-steam/protocol/steamlang"
)
func (b *Bsteam) handleChatMsg(e *steam.ChatMsgEvent) {
b.Log.Debugf("Receiving ChatMsgEvent: %#v", e)
b.Log.Debugf("<= Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account)
var channel int64
if e.ChatRoomId == 0 {
channel = int64(e.ChatterId)
} else {
// for some reason we have to remove 0x18000000000000
// TODO
// https://github.com/42wim/matterbridge/pull/630#discussion_r238102751
// channel = int64(e.ChatRoomId) & 0xfffffffffffff
channel = int64(e.ChatRoomId) - 0x18000000000000
}
msg := config.Message{
Username: b.getNick(e.ChatterId),
Text: e.Message,
Channel: strconv.FormatInt(channel, 10),
Account: b.Account,
UserID: strconv.FormatInt(int64(e.ChatterId), 10),
}
b.Remote <- msg
}
func (b *Bsteam) handleEvents() {
myLoginInfo := &steam.LogOnDetails{
Username: b.GetString("Login"),
Password: b.GetString("Password"),
AuthCode: b.GetString("AuthCode"),
}
// TODO Attempt to read existing auth hash to avoid steam guard.
// Maybe works
//myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry")
for event := range b.c.Events() {
switch e := event.(type) {
case *steam.ChatMsgEvent:
b.handleChatMsg(e)
case *steam.PersonaStateEvent:
b.Log.Debugf("PersonaStateEvent: %#v\n", e)
b.Lock()
b.userMap[e.FriendId] = e.Name
b.Unlock()
case *steam.ConnectedEvent:
b.c.Auth.LogOn(myLoginInfo)
case *steam.MachineAuthUpdateEvent:
// TODO sentry files for 2 auth
/*
b.Log.Info("authupdate", e)
b.Log.Info("hash", e.Hash)
ioutil.WriteFile("sentry", e.Hash, 0666)
*/
case *steam.LogOnFailedEvent:
b.Log.Info("Logon failed", e)
err := b.handleLogOnFailed(e, myLoginInfo)
if err != nil {
b.Log.Error(err)
return
}
case *steam.LoggedOnEvent:
b.Log.Debugf("LoggedOnEvent: %#v", e)
b.connected <- struct{}{}
b.Log.Debugf("setting online")
b.c.Social.SetPersonaState(steamlang.EPersonaState_Online)
case *steam.DisconnectedEvent:
b.Log.Info("Disconnected")
b.Log.Info("Attempting to reconnect...")
b.c.Connect()
case steam.FatalErrorEvent:
b.Log.Errorf("steam FatalErrorEvent: %#v", e)
default:
b.Log.Debugf("unknown event %#v", e)
}
}
}
func (b *Bsteam) handleLogOnFailed(e *steam.LogOnFailedEvent, myLoginInfo *steam.LogOnDetails) error {
switch e.Result {
case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode:
b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:")
var code string
fmt.Scanf("%s", &code)
// TODO https://github.com/42wim/matterbridge/pull/630#discussion_r238103978
myLoginInfo.TwoFactorCode = code
case steamlang.EResult_AccountLogonDenied:
b.Log.Info("Steam guard isn't letting me in! Enter auth code:")
var code string
fmt.Scanf("%s", &code)
// TODO https://github.com/42wim/matterbridge/pull/630#discussion_r238103978
myLoginInfo.AuthCode = code
case steamlang.EResult_InvalidLoginAuthCode:
return fmt.Errorf("Steam guard: invalid login auth code: %#v ", e.Result)
default:
return fmt.Errorf("LogOnFailedEvent: %#v ", e.Result)
// TODO: Handle EResult_InvalidLoginAuthCode
}
return nil
}
// handleFileInfo handles config.FileInfo and adds correct file comment or URL to msg.Text.
// Returns error if cast fails.
func (b *Bsteam) handleFileInfo(msg *config.Message, f interface{}) error {
if _, ok := f.(config.FileInfo); !ok {
return fmt.Errorf("handleFileInfo cast failed %#v", f)
}
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
return nil
}

View File

@ -2,7 +2,6 @@ package bsteam
import (
"fmt"
"strconv"
"sync"
"time"
@ -73,22 +72,13 @@ func (b *Bsteam) Send(msg config.Message) (string, error) {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, 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
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
for i := range msg.Extra["file"] {
if err := b.handleFileInfo(&msg, msg.Extra["file"][i]); err != nil {
b.Log.Error(err)
}
return "", nil
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
}
return "", nil
}
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
@ -103,78 +93,3 @@ func (b *Bsteam) getNick(id steamid.SteamId) string {
}
return "unknown"
}
func (b *Bsteam) handleEvents() {
myLoginInfo := new(steam.LogOnDetails)
myLoginInfo.Username = b.GetString("Login")
myLoginInfo.Password = b.GetString("Password")
myLoginInfo.AuthCode = b.GetString("AuthCode")
// Attempt to read existing auth hash to avoid steam guard.
// Maybe works
//myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry")
for event := range b.c.Events() {
//b.Log.Info(event)
switch e := event.(type) {
case *steam.ChatMsgEvent:
b.Log.Debugf("Receiving ChatMsgEvent: %#v", e)
b.Log.Debugf("<= Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account)
var channel int64
if e.ChatRoomId == 0 {
channel = int64(e.ChatterId)
} else {
// for some reason we have to remove 0x18000000000000
channel = int64(e.ChatRoomId) - 0x18000000000000
}
msg := config.Message{Username: b.getNick(e.ChatterId), Text: e.Message, Channel: strconv.FormatInt(channel, 10), Account: b.Account, UserID: strconv.FormatInt(int64(e.ChatterId), 10)}
b.Remote <- msg
case *steam.PersonaStateEvent:
b.Log.Debugf("PersonaStateEvent: %#v\n", e)
b.Lock()
b.userMap[e.FriendId] = e.Name
b.Unlock()
case *steam.ConnectedEvent:
b.c.Auth.LogOn(myLoginInfo)
case *steam.MachineAuthUpdateEvent:
/*
b.Log.Info("authupdate", e)
b.Log.Info("hash", e.Hash)
ioutil.WriteFile("sentry", e.Hash, 0666)
*/
case *steam.LogOnFailedEvent:
b.Log.Info("Logon failed", e)
switch e.Result {
case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode:
{
b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:")
var code string
fmt.Scanf("%s", &code)
myLoginInfo.TwoFactorCode = code
}
case steamlang.EResult_AccountLogonDenied:
{
b.Log.Info("Steam guard isn't letting me in! Enter auth code:")
var code string
fmt.Scanf("%s", &code)
myLoginInfo.AuthCode = code
}
default:
b.Log.Errorf("LogOnFailedEvent: %#v ", e.Result)
// TODO: Handle EResult_InvalidLoginAuthCode
return
}
case *steam.LoggedOnEvent:
b.Log.Debugf("LoggedOnEvent: %#v", e)
b.connected <- struct{}{}
b.Log.Debugf("setting online")
b.c.Social.SetPersonaState(steamlang.EPersonaState_Online)
case *steam.DisconnectedEvent:
b.Log.Info("Disconnected")
b.Log.Info("Attempting to reconnect...")
b.c.Connect()
case steam.FatalErrorEvent:
b.Log.Error(e)
default:
b.Log.Debugf("unknown event %#v", e)
}
}
}

377
bridge/telegram/handlers.go Normal file
View File

@ -0,0 +1,377 @@
package btelegram
import (
"html"
"regexp"
"strconv"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/go-telegram-bot-api/telegram-bot-api"
)
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
// handle channels
if posted != nil {
message = posted
rmsg.Text = message.Text
}
// edited channel message
if edited != nil && !b.GetBool("EditDisable") {
message = edited
rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix")
}
return message
}
// handleChannels checks if it's a channel message and if the message is a new or edited messages
func (b *Btelegram) handleChannels(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
return b.handleUpdate(rmsg, message, update.ChannelPost, update.EditedChannelPost)
}
// handleGroups checks if it's a group message and if the message is a new or edited messages
func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
return b.handleUpdate(rmsg, message, update.Message, update.EditedMessage)
}
// handleForwarded handles forwarded messages
func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) {
if message.ForwardFrom != nil {
usernameForward := ""
if b.GetBool("UseFirstName") {
usernameForward = message.ForwardFrom.FirstName
}
if usernameForward == "" {
usernameForward = message.ForwardFrom.UserName
if usernameForward == "" {
usernameForward = message.ForwardFrom.FirstName
}
}
if usernameForward == "" {
usernameForward = unknownUser
}
rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
}
}
// handleQuoting handles quoting of previous messages
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
if message.ReplyToMessage != nil {
usernameReply := ""
if message.ReplyToMessage.From != nil {
if b.GetBool("UseFirstName") {
usernameReply = message.ReplyToMessage.From.FirstName
}
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.UserName
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.FirstName
}
}
}
if usernameReply == "" {
usernameReply = unknownUser
}
if !b.GetBool("QuoteDisable") {
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
}
}
}
// handleUsername handles the correct setting of the username
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
if message.From != nil {
rmsg.UserID = strconv.Itoa(message.From.ID)
if b.GetBool("UseFirstName") {
rmsg.Username = message.From.FirstName
}
if rmsg.Username == "" {
rmsg.Username = message.From.UserName
if rmsg.Username == "" {
rmsg.Username = message.From.FirstName
}
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" {
b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
}
}
// if we really didn't find a username, set it to unknown
if rmsg.Username == "" {
rmsg.Username = unknownUser
}
}
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
for update := range updates {
b.Log.Debugf("== Receiving event: %#v", update.Message)
if update.Message == nil && update.ChannelPost == nil &&
update.EditedMessage == nil && update.EditedChannelPost == nil {
b.Log.Error("Getting nil messages, this shouldn't happen.")
continue
}
var message *tgbotapi.Message
rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
// handle channels
message = b.handleChannels(&rmsg, message, update)
// handle groups
message = b.handleGroups(&rmsg, message, update)
// set the ID's from the channel or group message
rmsg.ID = strconv.Itoa(message.MessageID)
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
// handle username
b.handleUsername(&rmsg, message)
// handle any downloads
err := b.handleDownload(&rmsg, message)
if err != nil {
b.Log.Errorf("download failed: %s", err)
}
// handle forwarded messages
b.handleForwarded(&rmsg, message)
// quote the previous message
b.handleQuoting(&rmsg, message)
// handle entities (adding URLs)
b.handleEntities(&rmsg, message)
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
// channels don't have (always?) user information. see #410
if message.From != nil {
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
}
}
// 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) {
rmsg := config.Message{Username: "system",
Text: "avatar",
Channel: channel,
Account: b.Account,
UserID: strconv.Itoa(userid),
Event: config.EventAvatarDownload,
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 {
b.Log.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"
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
if err != nil {
b.Log.Error(err)
return
}
data, err := helper.DownloadFile(url)
if err != nil {
b.Log.Errorf("download %s failed %#v", url, err)
return
}
helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
b.Remote <- rmsg
}
}
}
// handleDownloadFile handles file download
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
size := 0
var url, name, text string
switch {
case message.Sticker != nil:
text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
size = message.Sticker.FileSize
case message.Voice != nil:
text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
size = message.Voice.FileSize
case message.Video != nil:
text, name, url = b.getDownloadInfo(message.Video.FileID, "", true)
size = message.Video.FileSize
case message.Audio != nil:
text, name, url = b.getDownloadInfo(message.Audio.FileID, "", true)
size = message.Audio.FileSize
case message.Document != nil:
_, _, url = b.getDownloadInfo(message.Document.FileID, "", false)
size = message.Document.FileSize
name = message.Document.FileName
text = " " + message.Document.FileName + " : " + url
case message.Photo != nil:
photos := *message.Photo
size = photos[len(photos)-1].FileSize
text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
}
// if name is empty we didn't match a thing to download
if name == "" {
return nil
}
// use the URL instead of native upload
if b.GetBool("UseInsecureURL") {
b.Log.Debugf("Setting message text to :%s", text)
rmsg.Text += text
return nil
}
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
if err != nil {
return err
}
data, err := helper.DownloadFile(url)
if err != nil {
return err
}
if strings.HasSuffix(name, ".webp") && b.GetBool("MediaConvertWebPToPNG") {
b.Log.Debugf("WebP to PNG conversion enabled, converting %s", name)
err := helper.ConvertWebPToPNG(data)
if err != nil {
b.Log.Errorf("conversion failed: %s", err)
} else {
name = strings.Replace(name, ".webp", ".png", 1)
}
}
helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
return nil
}
func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (string, string, string) {
url := b.getFileDirectURL(id)
name := ""
if urlpart {
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
}
if suffix != "" && !strings.HasSuffix(name, suffix) {
name += suffix
}
text := " " + url
return text, name, url
}
// handleDelete handles message deleting
func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, error) {
if msg.ID == "" {
return "", nil
}
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
return "", err
}
// handleEdit handles message editing.
func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error) {
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
msg.Text = html.EscapeString(msg.Text)
}
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
switch b.GetString("MessageFormat") {
case HTMLFormat:
b.Log.Debug("Using mode HTML")
m.ParseMode = tgbotapi.ModeHTML
case "Markdown":
b.Log.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
m.ParseMode = tgbotapi.ModeHTML
}
_, err = b.c.Send(m)
if err != nil {
return "", err
}
return "", nil
}
// handleUploadFile handles native upload of files
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
var c tgbotapi.Chattable
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := tgbotapi.FileBytes{
Name: fi.Name,
Bytes: *fi.Data,
}
re := regexp.MustCompile(".(jpg|png)$")
if re.MatchString(fi.Name) {
c = tgbotapi.NewPhotoUpload(chatid, file)
} else {
c = tgbotapi.NewDocumentUpload(chatid, file)
}
_, err := b.c.Send(c)
if err != nil {
b.Log.Errorf("file upload failed: %#v", err)
}
if fi.Comment != "" {
if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil {
b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err)
}
}
}
return ""
}
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
format := b.GetString("quoteformat")
if format == "" {
format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
}
format = strings.Replace(format, "{MESSAGE}", message, -1)
format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1)
format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)
return format
}
// handleEntities handles messageEntities
func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Message) {
if message.Entities == nil {
return
}
// for now only do URL replacements
for _, e := range *message.Entities {
if e.Type == "text_link" {
url, err := e.ParseURL()
if err != nil {
b.Log.Errorf("entity text_link url parse failed: %s", err)
continue
}
link := rmsg.Text[e.Offset : e.Offset+e.Length]
rmsg.Text = strings.Replace(rmsg.Text, link, url.String(), 1)
}
}
}

View File

@ -3,7 +3,6 @@ package btelegram
import (
"bytes"
"html"
"io"
"github.com/russross/blackfriday"
)
@ -33,8 +32,8 @@ func (options *customHTML) Header(out *bytes.Buffer, text func() bool, level int
options.Paragraph(out, text)
}
func (options *customHTML) HRule(out io.ByteWriter) {
out.WriteByte('\n')
func (options *customHTML) HRule(out *bytes.Buffer) {
out.WriteByte('\n') //nolint:errcheck
}
func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) {
@ -54,16 +53,13 @@ func (options *customHTML) ListItem(out *bytes.Buffer, text []byte, flags int) {
}
func makeHTML(input string) string {
extensions := blackfriday.NoIntraEmphasis |
blackfriday.FencedCode |
blackfriday.Autolink |
blackfriday.SpaceHeadings |
blackfriday.HeadingIDs |
blackfriday.BackslashLineBreak |
blackfriday.DefinitionLists
renderer := &customHTML{blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
Flags: blackfriday.UseXHTML | blackfriday.SkipImages,
})}
return string(blackfriday.Run([]byte(input), blackfriday.WithExtensions(extensions), blackfriday.WithRenderer(renderer)))
return string(blackfriday.Markdown([]byte(input),
&customHTML{blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML|blackfriday.HTML_SKIP_IMAGES, "", "")},
blackfriday.EXTENSION_NO_INTRA_EMPHASIS|
blackfriday.EXTENSION_FENCED_CODE|
blackfriday.EXTENSION_AUTOLINK|
blackfriday.EXTENSION_SPACE_HEADERS|
blackfriday.EXTENSION_HEADER_IDS|
blackfriday.EXTENSION_BACKSLASH_LINE_BREAK|
blackfriday.EXTENSION_DEFINITION_LISTS))
}

View File

@ -2,7 +2,6 @@ package btelegram
import (
"html"
"regexp"
"strconv"
"strings"
@ -76,21 +75,15 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
return "", err
return b.handleDelete(&msg, chatid)
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.sendMessage(chatid, rmsg.Username, rmsg.Text)
if _, err := b.sendMessage(chatid, rmsg.Username, rmsg.Text); err != nil {
b.Log.Errorf("sendMessage failed: %s", err)
}
}
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
@ -100,162 +93,13 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
// edit the message if we have a msg ID
if msg.ID != "" {
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
msg.Text = html.EscapeString(msg.Text)
}
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
if b.GetString("MessageFormat") == HTMLFormat {
b.Log.Debug("Using mode HTML")
m.ParseMode = tgbotapi.ModeHTML
}
if b.GetString("MessageFormat") == "Markdown" {
b.Log.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
m.ParseMode = tgbotapi.ModeHTML
}
_, err = b.c.Send(m)
if err != nil {
return "", err
}
return "", nil
return b.handleEdit(&msg, chatid)
}
// Post normal message
return b.sendMessage(chatid, msg.Username, msg.Text)
}
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
for update := range updates {
b.Log.Debugf("== Receiving event: %#v", update.Message)
if update.Message == nil && update.ChannelPost == nil && update.EditedMessage == nil && update.EditedChannelPost == nil {
b.Log.Error("Getting nil messages, this shouldn't happen.")
continue
}
var message *tgbotapi.Message
rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
// handle channels
if update.ChannelPost != nil {
message = update.ChannelPost
rmsg.Text = message.Text
}
// edited channel message
if update.EditedChannelPost != nil && !b.GetBool("EditDisable") {
message = update.EditedChannelPost
rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix")
}
// handle groups
if update.Message != nil {
message = update.Message
rmsg.Text = message.Text
}
// edited group message
if update.EditedMessage != nil && !b.GetBool("EditDisable") {
message = update.EditedMessage
rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix")
}
// set the ID's from the channel or group message
rmsg.ID = strconv.Itoa(message.MessageID)
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
// handle username
if message.From != nil {
rmsg.UserID = strconv.Itoa(message.From.ID)
if b.GetBool("UseFirstName") {
rmsg.Username = message.From.FirstName
}
if rmsg.Username == "" {
rmsg.Username = message.From.UserName
if rmsg.Username == "" {
rmsg.Username = message.From.FirstName
}
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" {
b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
}
}
// if we really didn't find a username, set it to unknown
if rmsg.Username == "" {
rmsg.Username = unknownUser
}
// handle any downloads
err := b.handleDownload(message, &rmsg)
if err != nil {
b.Log.Errorf("download failed: %s", err)
}
// handle forwarded messages
if message.ForwardFrom != nil {
usernameForward := ""
if b.GetBool("UseFirstName") {
usernameForward = message.ForwardFrom.FirstName
}
if usernameForward == "" {
usernameForward = message.ForwardFrom.UserName
if usernameForward == "" {
usernameForward = message.ForwardFrom.FirstName
}
}
if usernameForward == "" {
usernameForward = unknownUser
}
rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
}
// quote the previous message
if message.ReplyToMessage != nil {
usernameReply := ""
if message.ReplyToMessage.From != nil {
if b.GetBool("UseFirstName") {
usernameReply = message.ReplyToMessage.From.FirstName
}
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.UserName
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.FirstName
}
}
}
if usernameReply == "" {
usernameReply = unknownUser
}
if !b.GetBool("QuoteDisable") {
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
}
}
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
// channels don't have (always?) user information. see #410
if message.From != nil {
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
}
}
func (b *Btelegram) getFileDirectURL(id string) string {
res, err := b.c.GetFileDirectURL(id)
if err != nil {
@ -264,146 +108,6 @@ func (b *Btelegram) getFileDirectURL(id string) string {
return res
}
// 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) {
rmsg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: strconv.Itoa(userid), Event: config.EventAvatarDownload, 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 {
b.Log.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"
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
if err != nil {
b.Log.Error(err)
return
}
data, err := helper.DownloadFile(url)
if err != nil {
b.Log.Errorf("download %s failed %#v", url, err)
return
}
helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
b.Remote <- rmsg
}
}
}
// handleDownloadFile handles file download
func (b *Btelegram) handleDownload(message *tgbotapi.Message, rmsg *config.Message) error {
size := 0
var url, name, text string
if message.Sticker != nil {
v := message.Sticker
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
if !strings.HasSuffix(name, ".webp") {
name += ".webp"
}
text = " " + url
}
if message.Video != nil {
v := message.Video
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
}
if message.Photo != nil {
photos := *message.Photo
size = photos[len(photos)-1].FileSize
url = b.getFileDirectURL(photos[len(photos)-1].FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
}
if message.Document != nil {
v := message.Document
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
name = v.FileName
text = " " + v.FileName + " : " + url
}
if message.Voice != nil {
v := message.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 += ".ogg"
}
}
if message.Audio != nil {
v := message.Audio
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
}
// if name is empty we didn't match a thing to download
if name == "" {
return nil
}
// use the URL instead of native upload
if b.GetBool("UseInsecureURL") {
b.Log.Debugf("Setting message text to :%s", text)
rmsg.Text += text
return nil
}
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
if err != nil {
return err
}
data, err := helper.DownloadFile(url)
if err != nil {
return err
}
helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
return nil
}
// handleUploadFile handles native upload of files
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) (string, error) {
var c tgbotapi.Chattable
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := tgbotapi.FileBytes{
Name: fi.Name,
Bytes: *fi.Data,
}
re := regexp.MustCompile(".(jpg|png)$")
if re.MatchString(fi.Name) {
c = tgbotapi.NewPhotoUpload(chatid, file)
} else {
c = tgbotapi.NewDocumentUpload(chatid, file)
}
_, err := b.c.Send(c)
if err != nil {
b.Log.Errorf("file upload failed: %#v", err)
}
if fi.Comment != "" {
b.sendMessage(chatid, msg.Username, fi.Comment)
}
}
return "", nil
}
func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) {
m := tgbotapi.NewMessage(chatid, "")
m.Text = username + text
@ -437,14 +141,3 @@ func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) {
}
return "", nil
}
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
format := b.GetString("quoteformat")
if format == "" {
format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
}
format = strings.Replace(format, "{MESSAGE}", message, -1)
format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1)
format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)
return format
}

104
bridge/whatsapp/handlers.go Normal file
View File

@ -0,0 +1,104 @@
package bwhatsapp
import (
"strings"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/matterbridge/go-whatsapp"
whatsappExt "github.com/matterbridge/mautrix-whatsapp/whatsapp-ext"
)
/*
Implement handling messages coming from WhatsApp
Check:
- https://github.com/Rhymen/go-whatsapp#add-message-handlers
- https://github.com/Rhymen/go-whatsapp/blob/master/handler.go
- https://github.com/tulir/mautrix-whatsapp/tree/master/whatsapp-ext for more advanced command handling
*/
// HandleError received from WhatsApp
func (b *Bwhatsapp) HandleError(err error) {
b.Log.Errorf("%v", err) // TODO implement proper handling? at least respond to different error types
}
// HandleTextMessage sent from WhatsApp, relay it to the brige
func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") {
return
}
// whatsapp sends last messages to show context , cut them
if message.Info.Timestamp < b.startedAt {
return
}
messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
groupJid := message.Info.RemoteJid
senderJid := message.Info.SenderJid
if len(senderJid) == 0 {
// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
senderJid = *message.Info.Source.Participant
}
// translate sender's Jid to the nicest username we can get
senderName := b.getSenderName(senderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
extText := message.Info.Source.Message.ExtendedTextMessage
if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil {
// handle user mentions
for _, mentionedJid := range extText.ContextInfo.MentionedJid {
numberAndSuffix := strings.SplitN(mentionedJid, "@", 2)
// mentions comes as telephone numbers and we don't want to expose it to other bridges
// replace it with something more meaninful to others
mention := b.getSenderNotify(numberAndSuffix[0] + whatsappExt.NewUserSuffix)
if mention == "" {
mention = "someone"
}
message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
}
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJid, b.Account)
rmsg := config.Message{
UserID: senderJid,
Username: senderName,
Text: message.Text,
Timestamp: messageTime,
Channel: groupJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
// Event string `json:"event"`
// Gateway string // will be added during message processing
ID: message.Info.Id}
if avatarURL, exists := b.userAvatars[senderJid]; exists {
rmsg.Avatar = avatarURL
}
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
//
//func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
// fmt.Println(message) // TODO implement
//}
//
//func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
// fmt.Println(message) // TODO implement
//}
//
//func (b *Bwhatsapp) HandleJsonMessage(message string) {
// fmt.Println(message) // TODO implement
//}
// TODO HandleRawMessage
// TODO HandleAudioMessage

View File

@ -0,0 +1,84 @@
package bwhatsapp
import (
"encoding/gob"
"errors"
"os"
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
"github.com/matterbridge/go-whatsapp"
)
func qrFromTerminal(invert bool) chan string {
qr := make(chan string)
go func() {
terminal := qrcodeTerminal.New()
if invert {
terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium)
}
terminal.Get(<-qr).Print()
}()
return qr
}
func (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
session := whatsapp.Session{}
sessionFile := b.Config.GetString(sessionFile)
if sessionFile == "" {
return session, errors.New("if you won't set SessionFile then you will need to scan QR code on every restart")
}
file, err := os.Open(sessionFile)
if err != nil {
return session, err
}
defer file.Close()
decoder := gob.NewDecoder(file)
err = decoder.Decode(&session)
if err != nil {
return session, err
}
return session, nil
}
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
sessionFile := b.Config.GetString(sessionFile)
if sessionFile == "" {
// we already sent a warning while starting the bridge, so let's be quiet here
return nil
}
file, err := os.Create(sessionFile)
if err != nil {
return err
}
defer file.Close()
encoder := gob.NewEncoder(file)
err = encoder.Encode(session)
return err
}
func (b *Bwhatsapp) getSenderName(senderJid string) string {
if sender, exists := b.users[senderJid]; exists {
if sender.Name != "" {
return sender.Name
}
// if user is not in phone contacts
// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
// users can change it in their WhatsApp settings -> profile -> click on Avatar
return sender.Notify
}
return ""
}
func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
if sender, exists := b.users[senderJid]; exists {
return sender.Notify
}
return ""
}

305
bridge/whatsapp/whatsapp.go Normal file
View File

@ -0,0 +1,305 @@
package bwhatsapp
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/matterbridge/go-whatsapp"
whatsappExt "github.com/matterbridge/mautrix-whatsapp/whatsapp-ext"
)
const (
// Account config parameters
cfgNumber = "Number"
qrOnWhiteTerminal = "QrOnWhiteTerminal"
sessionFile = "SessionFile"
)
// Bwhatsapp Bridge structure keeping all the information needed for relying
type Bwhatsapp struct {
*bridge.Config
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21
session *whatsapp.Session
conn *whatsapp.Conn
// https://github.com/tulir/mautrix-whatsapp/blob/master/whatsapp-ext/whatsapp.go
connExt *whatsappExt.ExtendedConn
startedAt uint64
users map[string]whatsapp.Contact
userAvatars map[string]string
}
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
func New(cfg *bridge.Config) bridge.Bridger {
number := cfg.GetString(cfgNumber)
if number == "" {
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
}
b := &Bwhatsapp{
Config: cfg,
users: make(map[string]whatsapp.Contact),
userAvatars: make(map[string]string),
}
return b
}
// Connect to WhatsApp. Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) Connect() error {
b.RLock() // TODO do we need locking for Whatsapp?
defer b.RUnlock()
number := b.GetString(cfgNumber)
if number == "" {
return errors.New("WhatsApp's telephone Number need to be configured")
}
// https://github.com/Rhymen/go-whatsapp#creating-a-connection
b.Log.Debugln("Connecting to WhatsApp..")
conn, err := whatsapp.NewConn(20 * time.Second)
if err != nil {
return errors.New("failed to connect to WhatsApp: " + err.Error())
}
b.conn = conn
b.connExt = whatsappExt.ExtendConn(b.conn)
// TODO do we want to use it? b.connExt.SetClientName("Matterbridge WhatsApp bridge", "mb-wa")
b.conn.AddHandler(b)
b.Log.Debugln("WhatsApp connection successful")
// load existing session in order to keep it between restarts
if b.session == nil {
var session whatsapp.Session
session, err = b.readSession()
if err == nil {
b.Log.Debugln("Restoring WhatsApp session..")
// https://github.com/Rhymen/go-whatsapp#restore
session, err = b.conn.RestoreSession(session)
if err != nil {
// TODO return or continue to normal login?
// restore session connection timed out (I couldn't get over it without logging in again)
return errors.New("failed to restore session: " + err.Error())
}
b.session = &session
b.Log.Debugln("Session restored successfully!")
} else {
b.Log.Warn(err.Error())
}
}
// login to a new session
if b.session == nil {
err = b.Login()
if err != nil {
return err
}
}
b.startedAt = uint64(time.Now().Unix())
_, err = b.conn.Contacts()
if err != nil {
return fmt.Errorf("error on update of contacts: %v", err)
}
// map all the users
for id, contact := range b.conn.Store.Contacts {
if !isGroupJid(id) && id != "status@broadcast" {
// it is user
b.users[id] = contact
}
}
// get user avatar asynchronously
go func() {
b.Log.Debug("Getting user avatars..")
for jid := range b.users {
info, err := b.connExt.GetProfilePicThumb(jid)
if err != nil {
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
} else {
// TODO any race conditions here?
b.userAvatars[jid] = info.URL
}
}
b.Log.Debug("Finished getting avatars..")
}()
return nil
}
// Login to WhatsApp creating a new session. This will require to scan a QR code on your mobile device
func (b *Bwhatsapp) Login() error {
b.Log.Debugln("Logging in..")
invert := b.GetBool(qrOnWhiteTerminal) // false is the default
qrChan := qrFromTerminal(invert)
session, err := b.conn.Login(qrChan)
if err != nil {
b.Log.Warnln("Failed to log in:", err)
return err
}
b.session = &session
b.Log.Infof("Logged into session: %#v", session)
b.Log.Infof("Connection: %#v", b.conn)
err = b.writeSession(session)
if err != nil {
fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
}
// TODO change connection strings to configured ones longClientName:"github.com/rhymen/go-whatsapp", shortClientName:"go-whatsapp"}" prefix=whatsapp
// TODO get also a nice logo
// TODO notification about unplugged and dead battery
// conn.Info: Wid, Pushname, Connected, Battery, Plugged
return nil
}
// Disconnect is called while reconnecting to the bridge
// TODO 42wim Documentation would be helpful on when reconnects happen and what should be done in this function
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) Disconnect() error {
// We could Logout, but that would close the session completely and would require a new QR code scan
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
return nil
}
func isGroupJid(identifier string) bool {
return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp")
}
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
byJid := isGroupJid(channel.Name)
// verify if we are member of the given group
if byJid {
// channel.Name specifies static group jID, not the name
if _, exists := b.conn.Store.Contacts[channel.Name]; !exists {
return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name)
}
} else {
// channel.Name specifies group name that might change, warn about it
var jids []string
for id, contact := range b.conn.Store.Contacts {
if isGroupJid(id) && contact.Name == channel.Name {
jids = append(jids, id)
}
}
switch len(jids) {
case 0:
// didn't match any group - print out possibilites
// TODO sort
// copy b;
//sort.Slice(people, func(i, j int) bool {
// return people[i].Age > people[j].Age
//})
for id, contact := range b.conn.Store.Contacts {
if isGroupJid(id) {
b.Log.Infof("%s %s", contact.Jid, contact.Name)
}
}
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
case 1:
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
default:
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
}
}
return nil
}
// Send a message from the bridge to WhatsApp
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
// No message ID in case action is executed on a message sent before the bridge was started
// and then the bridge cache doesn't have this message ID mapped
// TODO 42wim Doesn't the app get clogged with a ton of IDs after some time of running?
// WhatsApp allows to set any ID so in that case we could use external IDs and don't do mapping
// but external IDs are not set
return "", nil
}
// TODO delete message on WhatsApp https://github.com/Rhymen/go-whatsapp/issues/100
return "", nil
}
// Edit message
if msg.ID != "" {
b.Log.Debugf("updating message with id %s", msg.ID)
msg.Text += " (edited)"
// TODO handle edit as a message reply with updated text
}
//// TODO Handle Upload a file
//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 {
// return b.handleUploadFile(&msg, roomID)
// }
//}
// Post text message
text := whatsapp.TextMessage{
Info: whatsapp.MessageInfo{
RemoteJid: msg.Channel, // which equals to group id
},
Text: msg.Username + msg.Text,
}
b.Log.Debugf("=> Sending %#v", msg)
// create message ID
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
bytes := make([]byte, 10)
if _, err := rand.Read(bytes); err != nil {
b.Log.Warn(err.Error())
}
text.Info.Id = strings.ToUpper(hex.EncodeToString(bytes))
err := b.conn.Send(text)
return text.Info.Id, err
}
// TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76
//func (b *Bwhatsapp) Command(cmd string) string {
// return ""
//}

View File

@ -17,6 +17,7 @@ type Bxmpp struct {
xc *xmpp.Client
xmppMap map[string]string
*bridge.Config
startTime time.Time
}
func New(cfg *bridge.Config) bridge.Bridger {
@ -153,6 +154,7 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
func (b *Bxmpp) handleXMPP() error {
var ok bool
var msgid string
b.startTime = time.Now()
done := b.xmppKeepAlive()
defer close(done)
for {
@ -164,15 +166,27 @@ func (b *Bxmpp) handleXMPP() error {
case xmpp.Chat:
if v.Type == "groupchat" {
b.Log.Debugf("== Receiving %#v", v)
event := ""
// skip invalid messages
if b.skipMessage(v) {
continue
}
if strings.Contains(v.Text, "has set the subject to:") {
event = config.EventTopicChange
}
msgid = v.ID
if v.ReplaceID != "" {
msgid = v.ReplaceID
}
rmsg := config.Message{Username: b.parseNick(v.Remote), Text: v.Text, Channel: b.parseChannel(v.Remote), Account: b.Account, UserID: v.Remote, ID: msgid}
rmsg := config.Message{
Username: b.parseNick(v.Remote),
Text: v.Text,
Channel: b.parseChannel(v.Remote),
Account: b.Account,
UserID: v.Remote,
ID: msgid,
Event: event,
}
// check if we have an action event
rmsg.Text, ok = b.replaceAction(rmsg.Text)
@ -259,6 +273,11 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
return true
}
// do not show subjects on connect #732
if strings.Contains(message.Text, "has set the subject to:") && time.Since(b.startTime) < time.Second*5 {
return true
}
// skip delayed messages
t := time.Time{}
return message.Stamp != t

View File

@ -4,6 +4,8 @@ import (
"encoding/json"
"io/ioutil"
"strconv"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
@ -17,6 +19,7 @@ type Bzulip struct {
bot *gzb.Bot
streams map[int]string
*bridge.Config
sync.RWMutex
}
func New(cfg *bridge.Config) bridge.Bridger {
@ -100,14 +103,46 @@ func (b *Bzulip) getChannel(id int) string {
func (b *Bzulip) handleQueue() error {
for {
messages, _ := b.q.GetEvents()
messages, err := b.q.GetEvents()
switch err {
case gzb.BackoffError:
time.Sleep(time.Second * 5)
case gzb.NoJSONError:
b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds")
time.Sleep(time.Second * 10)
case gzb.BadEventQueueError:
b.Log.Info("got a bad event queue id error, reconnecting")
b.bot.Queues = nil
for {
b.q, err = b.bot.RegisterAll()
if err != nil {
b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
time.Sleep(time.Second * 10)
}
break
}
case gzb.HeartbeatError:
b.Log.Debug("heartbeat received.")
default:
b.Log.Debugf("receiving error: %#v", err)
}
if err != nil {
continue
}
for _, m := range messages {
b.Log.Debugf("== Receiving %#v", m)
// ignore our own messages
if m.SenderEmail == b.GetString("login") {
continue
}
rmsg := config.Message{Username: m.SenderFullName, Text: m.Content, Channel: b.getChannel(m.StreamID), Account: b.Account, UserID: strconv.Itoa(m.SenderID), Avatar: m.AvatarURL}
rmsg := config.Message{
Username: m.SenderFullName,
Text: m.Content,
Channel: b.getChannel(m.StreamID) + "/topic:" + m.Subject,
Account: b.Account,
UserID: strconv.Itoa(m.SenderID),
Avatar: m.AvatarURL,
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
@ -118,9 +153,11 @@ func (b *Bzulip) handleQueue() error {
}
func (b *Bzulip) sendMessage(msg config.Message) (string, error) {
topic := "matterbridge"
if b.GetString("topic") != "" {
topic = b.GetString("topic")
topic := ""
if strings.Contains(msg.Channel, "/topic:") {
res := strings.Split(msg.Channel, "/topic:")
topic = res[1]
msg.Channel = res[0]
}
m := gzb.Message{
Stream: msg.Channel,

View File

@ -1,3 +1,109 @@
# v1.14.1
## Bugfix
* slack: Fix crash double unlock (slack) (#771)
# v1.14.0
## Breaking
* zulip: Need to specify /topic:mytopic for channel configuration (zulip). (#751)
## New features
* whatsapp: new protocol added. Add initial WhatsApp support (#711) Thanks to @KrzysztofMadejski
* facebook messenger: new protocol via matterbridge api. See https://github.com/VictorNine/fbridge/ for more information.
* general: Add scripting (tengo) support for every incoming message (#731). See `TengoModifyMessage`
* general: Allow regexs in ignoreNicks. Closes #690 (#720)
* general: Support rewriting messages from relaybots using ExtractNicks. Fixes #466 (#730). See `ExtractNicks` in matterbridge.toml.sample
* general: refactor Make all loggers derive from non-default instance (#728). Thanks to @Helcaraxan
* rocketchat: add support for the rocketchat API. Sending to rocketchat now supports uploading of files, editing and deleting of messages.
* discord: Support join/leaves from discord. Closes #654 (#721)
* discord: Allow sending discriminator with Discord username (#726). See `UseDiscriminator` in matterbridge.toml.sample
* slack: Add extra debug option (slack). See `Debug` in the slack section in matterbridge.toml.sample
* telegram: Add support for URL in messageEntities (telegram). Fixes #735 (#736)
* telegram: Add MediaConvertWebPToPNG option (telegram). (#741). See `MediaConvertWebPToPNG` in matterbridge.toml.sample
## Enhancements
* general: Fail gracefully on incorrect human input. Fixes #739 (#740)
* matrix: Detect html nicks in RemoteNickFormat (matrix). Fixes #696 (#719)
* matrix: Send notices on join/parts (matrix). Fixes #712 (#716)
## Bugfix
* general: Handle file upload/download only once for each message (#742)
* zulip: Fix error handling on bad event queue id (zulip). Closes #694
* zulip: Keep reconnecting until succeed (zulip) (#737)
* irc: add support for (older) unrealircd versions. #708
* irc: Support quits from irc correctly. Fixes #722 (#724)
* matrix: Send username when uploading video/images (matrix). Fixes #715 (#717)
* matrix: Trim <p> and </p> tags (matrix). Closes #686 (#753)
* slack: Hint at thread replies when messages are unthreaded (slack) (#684)
* slack: Fix race-condition in populateUser() (#767)
* xmpp: Do not send topic changes on connect (xmpp). Fixes #732 (#733)
* telegram: Fix regression in HTML handling (telegram). Closes #734
* discord: Do not relay any bot messages (discord) (#743)
* rocketchat: Do not send duplicate messages (rocketchat). Fixes #745 (#752)
## Contributors
This release couldn't exist without the following contributors:
@Helcaraxan, @KrzysztofMadejski, @AJolly, @DeclanHoare
# v1.13.1
This release fixes go modules issues because of https://github.com/labstack/echo/issues/1272
## Bugfix
* general: fixes Unable to build 1.13.0 #698
* api: move to labstack/echo/v4 fixes #698
# v1.13.0
## New features
* general: refactors of telegram, irc, mattermost, matrix, discord, sshchat bridges and the gateway.
* irc: Add option to send RAW commands after connection (irc) #490. See `RunCommands` in matterbridge.toml.sample
* mattermost: 3.x support dropped
* mattermost: Add support for mattermost threading (#627)
* slack: Sync channel topics between Slack bridges #585. See `SyncTopic` in matterbridge.toml.sample
* matrix: Add support for markdown to HTML conversion (matrix). Closes #663 (#670)
* discord: Improve error reporting on failure to join Discord. Fixes #672 (#680)
* discord: Use only one webhook if possible (discord) (#681)
* discord: Allow to bridge non-bot Discord users (discord) (#689) If you prefix a token with `User ` it'll treat is as a user token.
## Bugfix
* slack: Try downloading files again if slack is too slow (slack). Closes #655 (#656)
* slack: Ignore LatencyReport event (slack)
* slack: Fix #668 strip lang in code fences sent to Slack (#673)
* sshchat: Fix sshchat connection logic (#661)
* sshchat: set quiet mode to filter joins/quits
* sshchat: Trim newlines in the end of relayed messages
* sshchat: fix media links
* sshchat: do not relay "Rate limiting is in effect" message
* mattermost: Fail if channel starts with hashtag (mattermost). Closes #625
* discord: Add file comment to webhook messages (discord). Fixes #358
* matrix: Fix displaying usernames for plain text clients. (matrix) (#685)
* irc: Fix possible data race (irc). Closes #693
* irc: Handle servers without MOTD (irc). Closes #692
# v1.12.3
## Bugfix
* slack: Fix bot (legacy token) messages not being send. Closes #571
* slack: Populate user on channel join (slack) (#644)
* slack: Add wait option for populateUsers/Channels (slack) Fixes #579 (#653)
# v1.12.2
## Bugfix
* irc: Fix multiple channel join regression. Closes #639
* slack: Make slack-legacy change less restrictive (#626)
# v1.12.1
## Bugfix
* discord: fix regression on server ID connection #619 #617
* discord: Limit discord username via webhook to 32 chars
* slack: Make sure threaded files stay in thread (slack). Fixes #590
* slack: Do not post empty messages (slack). Fixes #574
* slack: Handle deleted/edited thread starting messages (slack). Fixes #600 (#605)
* irc: Rework connection logic (irc)
* irc: Fix Nickserv logic (irc) #602
# v1.12.0
## Breaking changes

210
contrib/api.yaml Normal file
View File

@ -0,0 +1,210 @@
openapi: 3.0.0
info:
contact: {}
description: A read/write API for the Matterbridge chat bridge.
license:
name: Apache 2.0
url: 'https://github.com/42wim/matterbridge/blob/master/LICENSE'
title: Matterbridge API
version: "0.1.0-oas3"
paths:
/health:
get:
responses:
'200':
description: OK
content:
'*/*':
schema:
type: string
summary: Checks if the server is alive.
/message:
post:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/config.OutgoingMessageResponse'
summary: Create a message
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/config.OutgoingMessage'
description: Message object to create
required: true
/messages:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
items:
$ref: '#/components/schemas/config.IncomingMessage'
type: array
security:
- ApiKeyAuth: []
summary: List new messages
/stream:
get:
responses:
'200':
description: OK
content:
application/x-json-stream:
schema:
$ref: '#/components/schemas/config.IncomingMessage'
summary: Stream realtime messages
servers:
- url: /api
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
schemas:
config.IncomingMessage:
properties:
avatar:
description: URL to an avatar image
example: >-
https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
type: string
event:
description: >-
A specific matterbridge event. (see
https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
type: string
gateway:
description: Name of the gateway as configured in matterbridge.toml
example: mygateway
type: string
text:
description: Content of the message
example: 'Testing, testing, 1-2-3.'
type: string
username:
description: Human-readable username
example: alice
type: string
account:
description: Unique account name of format "[protocol].[slug]" as defined in matterbridge.toml
example: slack.myteam
type: string
channel:
description: Human-readable channel name of sending bridge
example: test-channel
type: string
id:
description: Unique ID of message on the gateway
example: slack 1541361213.030700
type: string
parent_id:
description: Unique ID of a parent message, if threaded
example: slack 1541361213.030700
type: string
protocol:
description: Chat protocol of the sending bridge
example: slack
type: string
timestamp:
description: Timestamp of the message
example: "1541361213.030700"
type: string
userid:
description: Userid on the sending bridge
example: U4MCXJKNC
type: string
extra:
description: Extra data that doesn't fit in other fields (eg base64 encoded files)
type: object
config.OutgoingMessage:
properties:
avatar:
description: URL to an avatar image
example: >-
https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
type: string
event:
description: >-
A specific matterbridge event. (see
https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
example: ""
type: string
gateway:
description: Name of the gateway as configured in matterbridge.toml
example: mygateway
type: string
text:
description: Content of the message
example: 'Testing, testing, 1-2-3.'
type: string
username:
description: Human-readable username
example: alice
type: string
type: object
required:
- gateway
- text
- username
config.OutgoingMessageResponse:
properties:
avatar:
description: URL to an avatar image
example: >-
https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
type: string
event:
description: >-
A specific matterbridge event. (see
https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
example: ""
type: string
gateway:
description: Name of the gateway as configured in matterbridge.toml
example: mygateway
type: string
text:
description: Content of the message
example: 'Testing, testing, 1-2-3.'
type: string
username:
description: Human-readable username
example: alice
type: string
account:
description: fixed api account
example: api.local
type: string
channel:
description: fixed api channel
example: api
type: string
id:
example: ""
type: string
parent_id:
example: ""
type: string
protocol:
description: fixed api protocol
example: api
type: string
timestamp:
description: Timestamp of the message
example: "1541361213.030700"
type: string
userid:
example: ""
type: string
extra:
example: null
type: object
type: object
security:
- bearerAuth: []

2
contrib/example.tengo Normal file
View File

@ -0,0 +1,2 @@
text := import("text")
msgText=text.re_replace("matterbridge",msgText,"matterbridge (https://github.com/42wim/matterbridge)")

View File

@ -1,11 +1,9 @@
FROM cmosh/alpine-arm:edge
ENTRYPOINT ["/bin/matterbridge"]
FROM alpine:edge as certs
RUN apk --update add ca-certificates
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
FROM scratch
ARG VERSION=1.12.3
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-linux-arm /bin/matterbridge
RUN chmod +x /bin/matterbridge
ENTRYPOINT ["/bin/matterbridge"]

5
gateway/bench.tengo Normal file
View File

@ -0,0 +1,5 @@
text := import("text")
if text.re_match("blah",msgText) {
msgText="replaced by this"
msgUsername="fakeuser"
}

View File

@ -0,0 +1,37 @@
package bridgemap
import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/api"
"github.com/42wim/matterbridge/bridge/discord"
"github.com/42wim/matterbridge/bridge/gitter"
"github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/matrix"
"github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/rocketchat"
"github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/sshchat"
"github.com/42wim/matterbridge/bridge/steam"
"github.com/42wim/matterbridge/bridge/telegram"
"github.com/42wim/matterbridge/bridge/whatsapp"
"github.com/42wim/matterbridge/bridge/xmpp"
"github.com/42wim/matterbridge/bridge/zulip"
)
var FullMap = map[string]bridge.Factory{
"api": api.New,
"discord": bdiscord.New,
"gitter": bgitter.New,
"irc": birc.New,
"mattermost": bmattermost.New,
"matrix": bmatrix.New,
"rocketchat": brocketchat.New,
"slack-legacy": bslack.NewLegacy,
"slack": bslack.New,
"sshchat": bsshchat.New,
"steam": bsteam.New,
"telegram": btelegram.New,
"whatsapp": bwhatsapp.New,
"xmpp": bxmpp.New,
"zulip": bzulip.New,
}

View File

@ -1,35 +1,18 @@
package gateway
import (
"bytes"
"crypto/sha1"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/api"
"github.com/42wim/matterbridge/bridge/config"
bdiscord "github.com/42wim/matterbridge/bridge/discord"
bgitter "github.com/42wim/matterbridge/bridge/gitter"
birc "github.com/42wim/matterbridge/bridge/irc"
bmatrix "github.com/42wim/matterbridge/bridge/matrix"
bmattermost "github.com/42wim/matterbridge/bridge/mattermost"
brocketchat "github.com/42wim/matterbridge/bridge/rocketchat"
bslack "github.com/42wim/matterbridge/bridge/slack"
bsshchat "github.com/42wim/matterbridge/bridge/sshchat"
bsteam "github.com/42wim/matterbridge/bridge/steam"
btelegram "github.com/42wim/matterbridge/bridge/telegram"
bxmpp "github.com/42wim/matterbridge/bridge/xmpp"
bzulip "github.com/42wim/matterbridge/bridge/zulip"
"github.com/hashicorp/golang-lru"
"github.com/d5/tengo/script"
lru "github.com/hashicorp/golang-lru"
"github.com/peterhellberg/emojilib"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
)
type Gateway struct {
@ -43,6 +26,8 @@ type Gateway struct {
Message chan config.Message
Name string
Messages *lru.Cache
logger *logrus.Entry
}
type BrMsgID struct {
@ -51,40 +36,30 @@ type BrMsgID struct {
ChannelID string
}
var flog *log.Entry
const apiProtocol = "api"
var bridgeMap = map[string]bridge.Factory{
"api": api.New,
"discord": bdiscord.New,
"gitter": bgitter.New,
"irc": birc.New,
"mattermost": bmattermost.New,
"matrix": bmatrix.New,
"rocketchat": brocketchat.New,
"slack-legacy": bslack.NewLegacy,
"slack": bslack.New,
"sshchat": bsshchat.New,
"steam": bsteam.New,
"telegram": btelegram.New,
"xmpp": bxmpp.New,
"zulip": bzulip.New,
}
// New creates a new Gateway object associated with the specified router and
// following the given configuration.
func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway {
logger := rootLogger.WithFields(logrus.Fields{"prefix": "gateway"})
const (
apiProtocol = "api"
)
func New(cfg config.Gateway, r *Router) *Gateway {
flog = log.WithFields(log.Fields{"prefix": "gateway"})
gw := &Gateway{Channels: make(map[string]*config.ChannelInfo), Message: r.Message,
Router: r, Bridges: make(map[string]*bridge.Bridge), Config: r.Config}
cache, _ := lru.New(5000)
gw.Messages = cache
gw.AddConfig(&cfg)
gw := &Gateway{
Channels: make(map[string]*config.ChannelInfo),
Message: r.Message,
Router: r,
Bridges: make(map[string]*bridge.Bridge),
Config: r.Config,
Messages: cache,
logger: logger,
}
if err := gw.AddConfig(cfg); err != nil {
logger.Errorf("Failed to add configuration to gateway: %#v", err)
}
return gw
}
// Find the canonical ID that the message is keyed under in cache
// FindCanonicalMsgID returns the ID under which a message was stored in the cache.
func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
ID := protocol + " " + mID
if gw.Messages.Contains(ID) {
@ -104,27 +79,36 @@ func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
return ""
}
// AddBridge sets up a new bridge in the gateway object with the specified configuration.
func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
br := gw.Router.getBridge(cfg.Account)
if br == nil {
br = bridge.New(cfg)
br.Config = gw.Router.Config
br.General = &gw.BridgeValues().General
// set logging
br.Log = log.WithFields(log.Fields{"prefix": "bridge"})
brconfig := &bridge.Config{Remote: gw.Message, Log: log.WithFields(log.Fields{"prefix": br.Protocol}), Bridge: br}
br.Log = gw.logger.WithFields(logrus.Fields{"prefix": br.Protocol})
brconfig := &bridge.Config{
Remote: gw.Message,
Bridge: br,
}
// add the actual bridger for this protocol to this bridge using the bridgeMap
br.Bridger = bridgeMap[br.Protocol](brconfig)
if _, ok := gw.Router.BridgeMap[br.Protocol]; !ok {
gw.logger.Fatalf("Incorrect protocol %s specified in gateway configuration %s, exiting.", br.Protocol, cfg.Account)
}
br.Bridger = gw.Router.BridgeMap[br.Protocol](brconfig)
}
gw.mapChannelsToBridge(br)
gw.Bridges[cfg.Account] = br
return nil
}
// AddConfig associates a new configuration with the gateway object.
func (gw *Gateway) AddConfig(cfg *config.Gateway) error {
gw.Name = cfg.Name
gw.MyConfig = cfg
gw.mapChannels()
if err := gw.mapChannels(); err != nil {
gw.logger.Errorf("mapChannels() failed: %s", err)
}
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
br := br //scopelint
err := gw.AddBridge(&br)
@ -144,18 +128,22 @@ func (gw *Gateway) mapChannelsToBridge(br *bridge.Bridge) {
}
func (gw *Gateway) reconnectBridge(br *bridge.Bridge) {
br.Disconnect()
if err := br.Disconnect(); err != nil {
gw.logger.Errorf("Disconnect() %s failed: %s", br.Account, err)
}
time.Sleep(time.Second * 5)
RECONNECT:
flog.Infof("Reconnecting %s", br.Account)
gw.logger.Infof("Reconnecting %s", br.Account)
err := br.Connect()
if err != nil {
flog.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
gw.logger.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
time.Sleep(time.Second * 60)
goto RECONNECT
}
br.Joined = make(map[string]bool)
br.JoinChannels()
if err := br.JoinChannels(); err != nil {
gw.logger.Errorf("JoinChannels() %s failed: %s", br.Account, err)
}
}
func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) {
@ -167,10 +155,24 @@ func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) {
if strings.HasPrefix(br.Account, "irc.") {
br.Channel = strings.ToLower(br.Channel)
}
if strings.HasPrefix(br.Account, "mattermost.") && strings.HasPrefix(br.Channel, "#") {
gw.logger.Errorf("Mattermost channels do not start with a #: remove the # in %s", br.Channel)
os.Exit(1)
}
if strings.HasPrefix(br.Account, "zulip.") && !strings.Contains(br.Channel, "/topic:") {
gw.logger.Errorf("Breaking change, since matterbridge 1.14.0 zulip channels need to specify the topic with channel/topic:mytopic in %s of %s", br.Channel, br.Account)
os.Exit(1)
}
ID := br.Channel + br.Account
if _, ok := gw.Channels[ID]; !ok {
channel := &config.ChannelInfo{Name: br.Channel, Direction: direction, ID: ID, Options: br.Options, Account: br.Account,
SameChannel: make(map[string]bool)}
channel := &config.ChannelInfo{
Name: br.Channel,
Direction: direction,
ID: ID,
Options: br.Options,
Account: br.Account,
SameChannel: make(map[string]bool),
}
channel.SameChannel[gw.Name] = br.SameChannel
gw.Channels[channel.ID] = channel
} else {
@ -198,10 +200,38 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
return channels
}
// discord join/leave is for the whole bridge, isn't a per channel join/leave
if msg.Event == config.EventJoinLeave && getProtocol(msg) == "discord" && msg.Channel == "" {
for _, channel := range gw.Channels {
if channel.Account == dest.Account && strings.Contains(channel.Direction, "out") &&
gw.validGatewayDest(msg) {
channels = append(channels, *channel)
}
}
return channels
}
// irc quit is for the whole bridge, isn't a per channel quit.
// channel is empty when we quit
if msg.Event == config.EventJoinLeave && getProtocol(msg) == "irc" && msg.Channel == "" {
// if we only have one channel on this irc bridge it's got to be the sending one.
// don't send it back
if dest.Account == msg.Account && len(dest.Channels) == 1 && dest.Protocol == "irc" {
return channels
}
for _, channel := range gw.Channels {
if channel.Account == dest.Account && strings.Contains(channel.Direction, "out") &&
gw.validGatewayDest(msg) {
channels = append(channels, *channel)
}
}
return channels
}
// if source channel is in only, do nothing
for _, channel := range gw.Channels {
// lookup the channel from the message
if channel.ID == getChannelID(*msg) {
if channel.ID == getChannelID(msg) {
// we only have destinations if the original message is from an "in" (sending) channel
if !strings.Contains(channel.Direction, "in") {
return channels
@ -210,11 +240,11 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
}
}
for _, channel := range gw.Channels {
if _, ok := gw.Channels[getChannelID(*msg)]; !ok {
if _, ok := gw.Channels[getChannelID(msg)]; !ok {
continue
}
// do samechannelgateway flogic
// do samechannelgateway logic
if channel.SameChannel[msg.Gateway] {
if msg.Channel == channel.Name && msg.Account != dest.Account {
channels = append(channels, *channel)
@ -228,7 +258,7 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
return channels
}
func (gw *Gateway) getDestMsgID(msgID string, dest *bridge.Bridge, channel config.ChannelInfo) string {
func (gw *Gateway) getDestMsgID(msgID string, dest *bridge.Bridge, channel *config.ChannelInfo) string {
if res, ok := gw.Messages.Get(msgID); ok {
IDs := res.([]*BrMsgID)
for _, id := range IDs {
@ -242,103 +272,23 @@ func (gw *Gateway) getDestMsgID(msgID string, dest *bridge.Bridge, channel confi
return ""
}
func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrMsgID {
var brMsgIDs []*BrMsgID
// if we have an attached file, or other info
if msg.Extra != nil {
if len(msg.Extra[config.EventFileFailureSize]) != 0 {
if msg.Text == "" {
return brMsgIDs
}
}
// ignoreTextEmpty returns true if we need to ignore a message with an empty text.
func (gw *Gateway) ignoreTextEmpty(msg *config.Message) bool {
if msg.Text != "" {
return false
}
// Avatar downloads are only relevant for telegram and mattermost for now
if msg.Event == config.EventAvatarDownload {
if dest.Protocol != "mattermost" &&
dest.Protocol != "telegram" {
return brMsgIDs
}
if msg.Event == config.EventUserTyping {
return false
}
// only relay join/part when configured
if msg.Event == config.EventJoinLeave && !gw.Bridges[dest.Account].GetBool("ShowJoinPart") {
return brMsgIDs
// we have an attachment or actual bytes, do not ignore
if msg.Extra != nil &&
(msg.Extra["attachments"] != nil ||
len(msg.Extra["file"]) > 0 ||
len(msg.Extra[config.EventFileFailureSize]) > 0) {
return false
}
// only relay topic change when configured
if msg.Event == config.EventTopicChange && !gw.Bridges[dest.Account].GetBool("ShowTopicChange") {
return brMsgIDs
}
// broadcast to every out channel (irc QUIT)
if msg.Channel == "" && msg.Event != config.EventJoinLeave {
flog.Debug("empty channel")
return brMsgIDs
}
// Get the ID of the parent message in thread
var canonicalParentMsgID string
if msg.ParentID != "" && (gw.BridgeValues().General.PreserveThreading || dest.GetBool("PreserveThreading")) {
canonicalParentMsgID = gw.FindCanonicalMsgID(msg.Protocol, msg.ParentID)
}
originchannel := msg.Channel
origmsg := msg
channels := gw.getDestChannel(&msg, *dest)
for _, channel := range channels {
// Only send the avatar download event to ourselves.
if msg.Event == config.EventAvatarDownload {
if channel.ID != getChannelID(origmsg) {
continue
}
} else {
// do not send to ourself for any other event
if channel.ID == getChannelID(origmsg) {
continue
}
}
// Too noisy to log like other events
if msg.Event != config.EventUserTyping {
flog.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name)
}
msg.Channel = channel.Name
msg.Avatar = gw.modifyAvatar(origmsg, dest)
msg.Username = gw.modifyUsername(origmsg, dest)
msg.ID = gw.getDestMsgID(origmsg.Protocol+" "+origmsg.ID, dest, channel)
// for api we need originchannel as channel
if dest.Protocol == apiProtocol {
msg.Channel = originchannel
}
msg.ParentID = gw.getDestMsgID(origmsg.Protocol+" "+canonicalParentMsgID, dest, channel)
if msg.ParentID == "" {
msg.ParentID = canonicalParentMsgID
}
// if we are using mattermost plugin account, send messages to MattermostPlugin channel
// that can be picked up by the mattermost matterbridge plugin
if dest.Account == "mattermost.plugin" {
gw.Router.MattermostPlugin <- msg
}
mID, err := dest.Send(msg)
if err != nil {
flog.Error(err)
}
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
if mID != "" {
flog.Debugf("mID %s: %s", dest.Account, mID)
brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID})
}
}
return brMsgIDs
gw.logger.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
return true
}
func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
@ -347,59 +297,23 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
return true
}
// check if we need to ignore a empty message
if msg.Text == "" {
if msg.Event == config.EventUserTyping {
return false
}
// we have an attachment or actual bytes, do not ignore
if msg.Extra != nil &&
(msg.Extra["attachments"] != nil ||
len(msg.Extra["file"]) > 0 ||
len(msg.Extra[config.EventFileFailureSize]) > 0) {
return false
}
flog.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks"))
igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages"))
if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) {
return true
}
// is the username in IgnoreNicks field
for _, entry := range strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks")) {
if msg.Username == entry {
flog.Debugf("ignoring %s from %s", msg.Username, msg.Account)
return true
}
}
// does the message match regex in IgnoreMessages field
// TODO do not compile regexps everytime
for _, entry := range strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages")) {
if entry != "" {
re, err := regexp.Compile(entry)
if err != nil {
flog.Errorf("incorrect regexp %s for %s", entry, msg.Account)
continue
}
if re.MatchString(msg.Text) {
flog.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account)
return true
}
}
}
return false
}
func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) string {
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string {
br := gw.Bridges[msg.Account]
msg.Protocol = br.Protocol
if gw.BridgeValues().General.StripNick || dest.GetBool("StripNick") {
if dest.GetBool("StripNick") {
re := regexp.MustCompile("[^a-zA-Z0-9]+")
msg.Username = re.ReplaceAllString(msg.Username, "")
}
nick := dest.GetString("RemoteNickFormat")
if nick == "" {
nick = gw.BridgeValues().General.RemoteNickFormat
}
// loop to replace nicks
for _, outer := range br.GetStringSlice2D("ReplaceNicks") {
@ -408,7 +322,7 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
// 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)
gw.logger.Errorf("regexp in %s failed: %s", msg.Account, err)
break
}
msg.Username = re.ReplaceAllString(msg.Username, replace)
@ -436,11 +350,8 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
return nick
}
func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string {
iconurl := gw.BridgeValues().General.IconURL
if iconurl == "" {
iconurl = dest.GetString("IconURL")
}
func (gw *Gateway) modifyAvatar(msg *config.Message, dest *bridge.Bridge) string {
iconurl := dest.GetString("IconURL")
iconurl = strings.Replace(iconurl, "{NICK}", msg.Username, -1)
if msg.Avatar == "" {
msg.Avatar = iconurl
@ -449,6 +360,10 @@ func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string
}
func (gw *Gateway) modifyMessage(msg *config.Message) {
if err := modifyMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil {
gw.logger.Errorf("TengoModifyMessage failed: %s", err)
}
// replace :emoji: to unicode
msg.Text = emojilib.Replace(msg.Text)
@ -460,108 +375,146 @@ func (gw *Gateway) modifyMessage(msg *config.Message) {
// 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)
gw.logger.Errorf("regexp in %s failed: %s", msg.Account, err)
break
}
msg.Text = re.ReplaceAllString(msg.Text, replace)
}
gw.handleExtractNicks(msg)
// messages from api have Gateway specified, don't overwrite
if msg.Protocol != apiProtocol {
msg.Gateway = gw.Name
}
}
// handleFiles uploads or places all files on the given msg to the MediaServer and
// adds the new URL of the file on the MediaServer onto the given msg.
func (gw *Gateway) handleFiles(msg *config.Message) {
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
// If we don't have a attachfield or we don't have a mediaserver configured return
if msg.Extra == nil ||
(gw.BridgeValues().General.MediaServerUpload == "" &&
gw.BridgeValues().General.MediaDownloadPath == "") {
return
}
// If we don't have files, nothing to upload.
if len(msg.Extra["file"]) == 0 {
return
}
client := &http.Client{
Timeout: time.Second * 5,
}
for i, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
ext := filepath.Ext(fi.Name)
fi.Name = fi.Name[0 : len(fi.Name)-len(ext)]
fi.Name = reg.ReplaceAllString(fi.Name, "_")
fi.Name += ext
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8]
if gw.BridgeValues().General.MediaServerUpload != "" {
// Use MediaServerUpload. Upload using a PUT HTTP request and basicauth.
url := gw.BridgeValues().General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
req, err := http.NewRequest("PUT", url, bytes.NewReader(*fi.Data))
if err != nil {
flog.Errorf("mediaserver upload failed, could not create request: %#v", err)
continue
}
flog.Debugf("mediaserver upload url: %s", url)
req.Header.Set("Content-Type", "binary/octet-stream")
_, err = client.Do(req)
if err != nil {
flog.Errorf("mediaserver upload failed, could not Do request: %#v", err)
continue
}
} else {
// Use MediaServerPath. Place the file on the current filesystem.
dir := gw.BridgeValues().General.MediaDownloadPath + "/" + sha1sum
err := os.Mkdir(dir, os.ModePerm)
if err != nil && !os.IsExist(err) {
flog.Errorf("mediaserver path failed, could not mkdir: %s %#v", err, err)
continue
}
path := dir + "/" + fi.Name
flog.Debugf("mediaserver path placing file: %s", path)
err = ioutil.WriteFile(path, *fi.Data, os.ModePerm)
if err != nil {
flog.Errorf("mediaserver path failed, could not writefile: %s %#v", err, err)
continue
}
// SendMessage sends a message (with specified parentID) to the channel on the selected
// destination bridge and returns a message ID or an error.
func (gw *Gateway) SendMessage(
rmsg *config.Message,
dest *bridge.Bridge,
channel *config.ChannelInfo,
canonicalParentMsgID string,
) (string, error) {
msg := *rmsg
// Only send the avatar download event to ourselves.
if msg.Event == config.EventAvatarDownload {
if channel.ID != getChannelID(rmsg) {
return "", nil
}
} else {
// do not send to ourself for any other event
if channel.ID == getChannelID(rmsg) {
return "", nil
}
// Download URL.
durl := gw.BridgeValues().General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
flog.Debugf("mediaserver download URL = %s", durl)
// We uploaded/placed the file successfully. Add the SHA and URL.
extra := msg.Extra["file"][i].(config.FileInfo)
extra.URL = durl
extra.SHA = sha1sum
msg.Extra["file"][i] = extra
}
// Too noisy to log like other events
if msg.Event != config.EventUserTyping {
gw.logger.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name)
}
msg.Channel = channel.Name
msg.Avatar = gw.modifyAvatar(rmsg, dest)
msg.Username = gw.modifyUsername(rmsg, dest)
msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel)
// for api we need originchannel as channel
if dest.Protocol == apiProtocol {
msg.Channel = rmsg.Channel
}
msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel)
if msg.ParentID == "" {
msg.ParentID = canonicalParentMsgID
}
// if the parentID is still empty and we have a parentID set in the original message
// this means that we didn't find it in the cache so set it "msg-parent-not-found"
if msg.ParentID == "" && rmsg.ParentID != "" {
msg.ParentID = "msg-parent-not-found"
}
// if we are using mattermost plugin account, send messages to MattermostPlugin channel
// that can be picked up by the mattermost matterbridge plugin
if dest.Account == "mattermost.plugin" {
gw.Router.MattermostPlugin <- msg
}
mID, err := dest.Send(msg)
if err != nil {
return mID, err
}
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
if mID != "" {
gw.logger.Debugf("mID %s: %s", dest.Account, mID)
return mID, nil
//brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID})
}
return "", nil
}
func (gw *Gateway) validGatewayDest(msg *config.Message) bool {
return msg.Gateway == gw.Name
}
func getChannelID(msg config.Message) string {
func getChannelID(msg *config.Message) string {
return msg.Channel + msg.Account
}
func isAPI(account string) bool {
return strings.HasPrefix(account, "api.")
}
// ignoreText returns true if text matches any of the input regexes.
func (gw *Gateway) ignoreText(text string, input []string) bool {
for _, entry := range input {
if entry == "" {
continue
}
// TODO do not compile regexps everytime
re, err := regexp.Compile(entry)
if err != nil {
gw.logger.Errorf("incorrect regexp %s", entry)
continue
}
if re.MatchString(text) {
gw.logger.Debugf("matching %s. ignoring %s", entry, text)
return true
}
}
return false
}
func getProtocol(msg *config.Message) string {
p := strings.Split(msg.Account, ".")
return p[0]
}
func modifyMessageTengo(filename string, msg *config.Message) error {
if filename == "" {
return nil
}
res, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
s := script.New(res)
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel)
c, err := s.Compile()
if err != nil {
return err
}
if err := c.Run(); err != nil {
return err
}
msg.Text = c.Get("msgText").String()
msg.Username = c.Get("msgUsername").String()
return nil
}

View File

@ -2,12 +2,15 @@ package gateway
import (
"fmt"
"io/ioutil"
"strconv"
"testing"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway/bridgemap"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/suite"
)
var testconfig = []byte(`
@ -159,8 +162,10 @@ const (
)
func maketestRouter(input []byte) *Router {
cfg := config.NewConfigFromString(input)
r, err := NewRouter(cfg)
logger := logrus.New()
logger.SetOutput(ioutil.Discard)
cfg := config.NewConfigFromString(logger, input)
r, err := NewRouter(logger, cfg, bridgemap.FullMap)
if err != nil {
fmt.Println(err)
}
@ -386,3 +391,139 @@ func TestGetDestChannelAdvanced(t *testing.T) {
}
assert.Equal(t, map[string]int{"bridge3": 4, "bridge": 9, "announcements": 3, "bridge2": 4}, hits)
}
type ignoreTestSuite struct {
suite.Suite
gw *Gateway
}
func TestIgnoreSuite(t *testing.T) {
s := &ignoreTestSuite{}
suite.Run(t, s)
}
func (s *ignoreTestSuite) SetupSuite() {
logger := logrus.New()
logger.SetOutput(ioutil.Discard)
s.gw = &Gateway{logger: logrus.NewEntry(logger)}
}
func (s *ignoreTestSuite) TestIgnoreTextEmpty() {
extraFile := make(map[string][]interface{})
extraAttach := make(map[string][]interface{})
extraFailure := make(map[string][]interface{})
extraFile["file"] = append(extraFile["file"], config.FileInfo{})
extraAttach["attachments"] = append(extraAttach["attachments"], []string{})
extraFailure[config.EventFileFailureSize] = append(extraFailure[config.EventFileFailureSize], config.FileInfo{})
msgTests := map[string]struct {
input *config.Message
output bool
}{
"usertyping": {
input: &config.Message{Event: config.EventUserTyping},
output: false,
},
"file attach": {
input: &config.Message{Extra: extraFile},
output: false,
},
"attachments": {
input: &config.Message{Extra: extraAttach},
output: false,
},
config.EventFileFailureSize: {
input: &config.Message{Extra: extraFailure},
output: false,
},
"nil extra": {
input: &config.Message{Extra: nil},
output: true,
},
"empty": {
input: &config.Message{},
output: true,
},
}
for testname, testcase := range msgTests {
output := s.gw.ignoreTextEmpty(testcase.input)
s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)
}
}
func (s *ignoreTestSuite) TestIgnoreTexts() {
msgTests := map[string]struct {
input string
re []string
output bool
}{
"no regex": {
input: "a text message",
re: []string{},
output: false,
},
"simple regex": {
input: "a text message",
re: []string{"text"},
output: true,
},
"multiple regex fail": {
input: "a text message",
re: []string{"abc", "123$"},
output: false,
},
"multiple regex pass": {
input: "a text message",
re: []string{"lala", "sage$"},
output: true,
},
}
for testname, testcase := range msgTests {
output := s.gw.ignoreText(testcase.input, testcase.re)
s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)
}
}
func (s *ignoreTestSuite) TestIgnoreNicks() {
msgTests := map[string]struct {
input string
re []string
output bool
}{
"no entry": {
input: "user",
re: []string{},
output: false,
},
"one entry": {
input: "user",
re: []string{"user"},
output: true,
},
"multiple entries": {
input: "user",
re: []string{"abc", "user"},
output: true,
},
"multiple entries fail": {
input: "user",
re: []string{"abc", "def"},
output: false,
},
}
for testname, testcase := range msgTests {
output := s.gw.ignoreText(testcase.input, testcase.re)
s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)
}
}
func BenchmarkTengo(b *testing.B) {
msg := &config.Message{Username: "user", Text: "blah testing", Account: "protocol.account", Channel: "mychannel"}
for n := 0; n < b.N; n++ {
err := modifyMessageTengo("bench.tengo", msg)
if err != nil {
return
}
}
}

266
gateway/handlers.go Normal file
View File

@ -0,0 +1,266 @@
package gateway
import (
"bytes"
"crypto/sha1" //nolint:gosec
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
)
// handleEventFailure handles failures and reconnects bridges.
func (r *Router) handleEventFailure(msg *config.Message) {
if msg.Event != config.EventFailure {
return
}
for _, gw := range r.Gateways {
for _, br := range gw.Bridges {
if msg.Account == br.Account {
go gw.reconnectBridge(br)
return
}
}
}
}
// handleEventGetChannelMembers handles channel members
func (r *Router) handleEventGetChannelMembers(msg *config.Message) {
if msg.Event != config.EventGetChannelMembers {
return
}
for _, gw := range r.Gateways {
for _, br := range gw.Bridges {
if msg.Account == br.Account {
cMembers := msg.Extra[config.EventGetChannelMembers][0].(config.ChannelMembers)
r.logger.Debugf("Syncing channelmembers from %s", msg.Account)
br.SetChannelMembers(&cMembers)
return
}
}
}
}
// handleEventRejoinChannels handles rejoining of channels.
func (r *Router) handleEventRejoinChannels(msg *config.Message) {
if msg.Event != config.EventRejoinChannels {
return
}
for _, gw := range r.Gateways {
for _, br := range gw.Bridges {
if msg.Account == br.Account {
br.Joined = make(map[string]bool)
if err := br.JoinChannels(); err != nil {
r.logger.Errorf("channel join failed for %s: %s", msg.Account, err)
}
}
}
}
}
// handleFiles uploads or places all files on the given msg to the MediaServer and
// adds the new URL of the file on the MediaServer onto the given msg.
func (gw *Gateway) handleFiles(msg *config.Message) {
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
// If we don't have a attachfield or we don't have a mediaserver configured return
if msg.Extra == nil ||
(gw.BridgeValues().General.MediaServerUpload == "" &&
gw.BridgeValues().General.MediaDownloadPath == "") {
return
}
// If we don't have files, nothing to upload.
if len(msg.Extra["file"]) == 0 {
return
}
for i, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
ext := filepath.Ext(fi.Name)
fi.Name = fi.Name[0 : len(fi.Name)-len(ext)]
fi.Name = reg.ReplaceAllString(fi.Name, "_")
fi.Name += ext
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8] //nolint:gosec
if gw.BridgeValues().General.MediaServerUpload != "" {
// Use MediaServerUpload. Upload using a PUT HTTP request and basicauth.
if err := gw.handleFilesUpload(&fi); err != nil {
gw.logger.Error(err)
continue
}
} else {
// Use MediaServerPath. Place the file on the current filesystem.
if err := gw.handleFilesLocal(&fi); err != nil {
gw.logger.Error(err)
continue
}
}
// Download URL.
durl := gw.BridgeValues().General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
gw.logger.Debugf("mediaserver download URL = %s", durl)
// We uploaded/placed the file successfully. Add the SHA and URL.
extra := msg.Extra["file"][i].(config.FileInfo)
extra.URL = durl
extra.SHA = sha1sum
msg.Extra["file"][i] = extra
}
}
// handleFilesUpload uses MediaServerUpload configuration to upload the file.
// Returns error on failure.
func (gw *Gateway) handleFilesUpload(fi *config.FileInfo) error {
client := &http.Client{
Timeout: time.Second * 5,
}
// Use MediaServerUpload. Upload using a PUT HTTP request and basicauth.
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8] //nolint:gosec
url := gw.BridgeValues().General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
req, err := http.NewRequest("PUT", url, bytes.NewReader(*fi.Data))
if err != nil {
return fmt.Errorf("mediaserver upload failed, could not create request: %#v", err)
}
gw.logger.Debugf("mediaserver upload url: %s", url)
req.Header.Set("Content-Type", "binary/octet-stream")
_, err = client.Do(req)
if err != nil {
return fmt.Errorf("mediaserver upload failed, could not Do request: %#v", err)
}
return nil
}
// handleFilesLocal use MediaServerPath configuration, places the file on the current filesystem.
// Returns error on failure.
func (gw *Gateway) handleFilesLocal(fi *config.FileInfo) error {
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8] //nolint:gosec
dir := gw.BridgeValues().General.MediaDownloadPath + "/" + sha1sum
err := os.Mkdir(dir, os.ModePerm)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("mediaserver path failed, could not mkdir: %s %#v", err, err)
}
path := dir + "/" + fi.Name
gw.logger.Debugf("mediaserver path placing file: %s", path)
err = ioutil.WriteFile(path, *fi.Data, os.ModePerm)
if err != nil {
return fmt.Errorf("mediaserver path failed, could not writefile: %s %#v", err, err)
}
return nil
}
// ignoreEvent returns true if we need to ignore this event for the specified destination bridge.
func (gw *Gateway) ignoreEvent(event string, dest *bridge.Bridge) bool {
switch event {
case config.EventAvatarDownload:
// Avatar downloads are only relevant for telegram and mattermost for now
if dest.Protocol != "mattermost" && dest.Protocol != "telegram" {
return true
}
case config.EventJoinLeave:
// only relay join/part when configured
if !dest.GetBool("ShowJoinPart") {
return true
}
case config.EventTopicChange:
// only relay topic change when used in some way on other side
if dest.GetBool("ShowTopicChange") && dest.GetBool("SyncTopic") {
return true
}
}
return false
}
// handleMessage makes sure the message get sent to the correct bridge/channels.
// Returns an array of msg ID's
func (gw *Gateway) handleMessage(rmsg *config.Message, dest *bridge.Bridge) []*BrMsgID {
var brMsgIDs []*BrMsgID
// if we have an attached file, or other info
if rmsg.Extra != nil && len(rmsg.Extra[config.EventFileFailureSize]) != 0 && rmsg.Text == "" {
return brMsgIDs
}
if gw.ignoreEvent(rmsg.Event, dest) {
return brMsgIDs
}
// broadcast to every out channel (irc QUIT)
if rmsg.Channel == "" && rmsg.Event != config.EventJoinLeave {
gw.logger.Debug("empty channel")
return brMsgIDs
}
// Get the ID of the parent message in thread
var canonicalParentMsgID string
if rmsg.ParentID != "" && dest.GetBool("PreserveThreading") {
canonicalParentMsgID = gw.FindCanonicalMsgID(rmsg.Protocol, rmsg.ParentID)
}
channels := gw.getDestChannel(rmsg, *dest)
for idx := range channels {
channel := &channels[idx]
msgID, err := gw.SendMessage(rmsg, dest, channel, canonicalParentMsgID)
if err != nil {
gw.logger.Errorf("SendMessage failed: %s", err)
continue
}
if msgID == "" {
continue
}
brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + msgID, channel.ID})
}
return brMsgIDs
}
func (gw *Gateway) handleExtractNicks(msg *config.Message) {
var err error
br := gw.Bridges[msg.Account]
for _, outer := range br.GetStringSlice2D("ExtractNicks") {
search := outer[0]
replace := outer[1]
msg.Username, msg.Text, err = extractNick(search, replace, msg.Username, msg.Text)
if err != nil {
gw.logger.Errorf("regexp in %s failed: %s", msg.Account, err)
break
}
}
}
// extractNick searches for a username (based on "search" a regular expression).
// if this matches it extracts a nick (based on "extract" another regular expression) from text
// and replaces username with this result.
// returns error if the regexp doesn't compile.
func extractNick(search, extract, username, text string) (string, string, error) {
re, err := regexp.Compile(search)
if err != nil {
return username, text, err
}
if re.MatchString(username) {
re, err = regexp.Compile(extract)
if err != nil {
return username, text, err
}
res := re.FindAllStringSubmatch(text, 1)
// only replace if we have exactly 1 match
if len(res) > 0 && len(res[0]) == 2 {
username = res[0][1]
text = strings.Replace(text, res[0][0], "", 1)
}
}
return username, text, nil
}

75
gateway/handlers_test.go Normal file
View File

@ -0,0 +1,75 @@
package gateway
import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/stretchr/testify/assert"
"testing"
)
func TestIgnoreEvent(t *testing.T) {
eventTests := map[string]struct {
input string
dest *bridge.Bridge
output bool
}{
"avatar mattermost": {
input: config.EventAvatarDownload,
dest: &bridge.Bridge{Protocol: "mattermost"},
output: false,
},
"avatar slack": {
input: config.EventAvatarDownload,
dest: &bridge.Bridge{Protocol: "slack"},
output: true,
},
"avatar telegram": {
input: config.EventAvatarDownload,
dest: &bridge.Bridge{Protocol: "telegram"},
output: false,
},
}
gw := &Gateway{}
for testname, testcase := range eventTests {
output := gw.ignoreEvent(testcase.input, testcase.dest)
assert.Equalf(t, testcase.output, output, "case '%s' failed", testname)
}
}
func TestExtractNick(t *testing.T) {
eventTests := map[string]struct {
search string
extract string
username string
text string
resultUsername string
resultText string
}{
"test1": {
search: "fromgitter",
extract: "<(.*?)>\\s+",
username: "fromgitter",
text: "<userx> blahblah",
resultUsername: "userx",
resultText: "blahblah",
},
"test2": {
search: "<.*?bot>",
//extract: `\((.*?)\)\s+`,
extract: "\\((.*?)\\)\\s+",
username: "<matterbot>",
text: "(userx) blahblah (abc) test",
resultUsername: "userx",
resultText: "blahblah (abc) test",
},
}
// gw := &Gateway{}
for testname, testcase := range eventTests {
resultUsername, resultText, _ := extractNick(testcase.search, testcase.extract, testcase.username, testcase.text)
assert.Equalf(t, testcase.resultUsername, resultUsername, "case '%s' failed", testname)
assert.Equalf(t, testcase.resultText, resultText, "case '%s' failed", testname)
}
}

View File

@ -2,32 +2,45 @@ package gateway
import (
"fmt"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
samechannelgateway "github.com/42wim/matterbridge/gateway/samechannel"
"github.com/42wim/matterbridge/gateway/samechannel"
"github.com/sirupsen/logrus"
)
type Router struct {
config.Config
sync.RWMutex
BridgeMap map[string]bridge.Factory
Gateways map[string]*Gateway
Message chan config.Message
MattermostPlugin chan config.Message
logger *logrus.Entry
}
func NewRouter(cfg config.Config) (*Router, error) {
// NewRouter initializes a new Matterbridge router for the specified configuration and
// sets up all required gateways.
func NewRouter(rootLogger *logrus.Logger, cfg config.Config, bridgeMap map[string]bridge.Factory) (*Router, error) {
logger := rootLogger.WithFields(logrus.Fields{"prefix": "router"})
r := &Router{
Config: cfg,
BridgeMap: bridgeMap,
Message: make(chan config.Message),
MattermostPlugin: make(chan config.Message),
Gateways: make(map[string]*Gateway),
logger: logger,
}
sgw := samechannelgateway.New(cfg)
gwconfigs := sgw.GetConfig()
sgw := samechannel.New(cfg)
gwconfigs := append(sgw.GetConfig(), cfg.BridgeValues().Gateway...)
for _, entry := range append(gwconfigs, cfg.BridgeValues().Gateway...) {
for idx := range gwconfigs {
entry := &gwconfigs[idx]
if !entry.Enable {
continue
}
@ -37,34 +50,66 @@ func NewRouter(cfg config.Config) (*Router, error) {
if _, ok := r.Gateways[entry.Name]; ok {
return nil, fmt.Errorf("Gateway with name %s already exists", entry.Name)
}
r.Gateways[entry.Name] = New(entry, r)
r.Gateways[entry.Name] = New(rootLogger, entry, r)
}
return r, nil
}
// Start will connect all gateways belonging to this router and subsequently route messages
// between them.
func (r *Router) Start() error {
m := make(map[string]*bridge.Bridge)
for _, gw := range r.Gateways {
flog.Infof("Parsing gateway %s", gw.Name)
r.logger.Infof("Parsing gateway %s", gw.Name)
for _, br := range gw.Bridges {
m[br.Account] = br
}
}
for _, br := range m {
flog.Infof("Starting bridge: %s ", br.Account)
r.logger.Infof("Starting bridge: %s ", br.Account)
err := br.Connect()
if err != nil {
return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err)
e := fmt.Errorf("Bridge %s failed to start: %v", br.Account, err)
if r.disableBridge(br, e) {
continue
}
return e
}
err = br.JoinChannels()
if err != nil {
return fmt.Errorf("Bridge %s failed to join channel: %v", br.Account, err)
e := fmt.Errorf("Bridge %s failed to join channel: %v", br.Account, err)
if r.disableBridge(br, e) {
continue
}
return e
}
}
// remove unused bridges
for _, gw := range r.Gateways {
for i, br := range gw.Bridges {
if br.Bridger == nil {
r.logger.Errorf("removing failed bridge %s", i)
delete(gw.Bridges, i)
}
}
}
go r.handleReceive()
//go r.updateChannelMembers()
return nil
}
// disableBridge returns true and empties a bridge if we have IgnoreFailureOnStart configured
// otherwise returns false
func (r *Router) disableBridge(br *bridge.Bridge, err error) bool {
if r.BridgeValues().General.IgnoreFailureOnStart {
r.logger.Error(err)
// setting this bridge empty
*br = bridge.Bridge{}
return true
}
return false
}
func (r *Router) getBridge(account string) *bridge.Bridge {
for _, gw := range r.Gateways {
if br, ok := gw.Bridges[account]; ok {
@ -77,42 +122,51 @@ func (r *Router) getBridge(account string) *bridge.Bridge {
func (r *Router) handleReceive() {
for msg := range r.Message {
msg := msg // scopelint
if msg.Event == config.EventFailure {
Loop:
for _, gw := range r.Gateways {
for _, br := range gw.Bridges {
if msg.Account == br.Account {
go gw.reconnectBridge(br)
break Loop
}
}
}
}
if msg.Event == config.EventRejoinChannels {
for _, gw := range r.Gateways {
for _, br := range gw.Bridges {
if msg.Account == br.Account {
br.Joined = make(map[string]bool)
br.JoinChannels()
}
}
}
}
r.handleEventGetChannelMembers(&msg)
r.handleEventFailure(&msg)
r.handleEventRejoinChannels(&msg)
idx := 0
for _, gw := range r.Gateways {
// record all the message ID's of the different bridges
var msgIDs []*BrMsgID
if !gw.ignoreMessage(&msg) {
msg.Timestamp = time.Now()
gw.modifyMessage(&msg)
gw.handleFiles(&msg)
for _, br := range gw.Bridges {
msgIDs = append(msgIDs, gw.handleMessage(msg, br)...)
}
// only add the message ID if it doesn't already exists
if _, ok := gw.Messages.Get(msg.Protocol + " " + msg.ID); !ok && msg.ID != "" {
gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs)
}
if gw.ignoreMessage(&msg) {
continue
}
msg.Timestamp = time.Now()
gw.modifyMessage(&msg)
if idx == 0 {
gw.handleFiles(&msg)
}
for _, br := range gw.Bridges {
msgIDs = append(msgIDs, gw.handleMessage(&msg, br)...)
}
// only add the message ID if it doesn't already exists
if _, ok := gw.Messages.Get(msg.Protocol + " " + msg.ID); !ok && msg.ID != "" {
gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs)
}
idx++
}
}
}
// updateChannelMembers sends every minute an GetChannelMembers event to all bridges.
func (r *Router) updateChannelMembers() {
// TODO sleep a minute because slack can take a while
// fix this by having actually connectionDone events send to the router
time.Sleep(time.Minute)
for {
for _, gw := range r.Gateways {
for _, br := range gw.Bridges {
// only for slack now
if br.Protocol != "slack" {
continue
}
r.logger.Debugf("sending %s to %s", config.EventGetChannelMembers, br.Account)
if _, err := br.Send(config.Message{Event: config.EventGetChannelMembers}); err != nil {
r.logger.Errorf("updateChannelMembers: %s", err)
}
}
}
time.Sleep(time.Minute)
}
}

View File

@ -1,4 +1,4 @@
package samechannelgateway
package samechannel
import (
"github.com/42wim/matterbridge/bridge/config"

View File

@ -1,10 +1,12 @@
package samechannelgateway
package samechannel
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/stretchr/testify/assert"
"io/ioutil"
"testing"
"github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
const testConfig = `
@ -66,7 +68,9 @@ var (
)
func TestGetConfig(t *testing.T) {
cfg := config.NewConfigFromString([]byte(testConfig))
logger := logrus.New()
logger.SetOutput(ioutil.Discard)
cfg := config.NewConfigFromString(logger, []byte(testConfig))
sgw := New(cfg)
configs := sgw.GetConfig()
assert.Equal(t, []config.Gateway{expectedConfig}, configs)

88
go.mod
View File

@ -2,79 +2,73 @@ module github.com/42wim/matterbridge
require (
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 // indirect
github.com/Philipp15b/go-steam v0.0.0-20161020161927-e0f3bb9566e3
github.com/alecthomas/log4go v0.0.0-20160307011253-e5dc62318d9b // indirect
github.com/Jeffail/gabs v1.1.1 // indirect
github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329
github.com/bwmarrin/discordgo v0.19.0
github.com/dfordsoft/golib v0.0.0-20180313113957-2ea3495aee1d
github.com/dgrijalva/jwt-go v0.0.0-20170508165458-6c8dedd55f8a // indirect
github.com/d5/tengo v1.12.1
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec
github.com/fsnotify/fsnotify v1.4.7
github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180428185002-212b1541150c
github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc // indirect
github.com/google/gops v0.0.0-20170319002943-62f833fc9f6c
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible
github.com/google/gops v0.3.5
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect
github.com/gorilla/schema v0.0.0-20170317173100-f3c80893412c
github.com/gorilla/schema v1.0.2
github.com/gorilla/websocket v1.4.0
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad
github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb // indirect
github.com/hashicorp/golang-lru v0.5.0
github.com/hpcloud/tail v1.0.0 // indirect
github.com/jpillora/backoff v0.0.0-20170222002228-06c7a16c845d
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/kardianos/osext v0.0.0-20170207191655-9b883c5eb462 // indirect
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/labstack/echo v0.0.0-20180219162101-7eec915044a1
github.com/labstack/gommon v0.2.1 // indirect
github.com/lrstanley/girc v0.0.0-20180913221000-0fb5b684054e
github.com/labstack/echo/v4 v4.0.0
github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect
github.com/magiconair/properties v0.0.0-20180217134545-2c9e95027885 // indirect
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d
github.com/matterbridge/go-whatsapp v0.0.1-0.20190301204034-f2f1b29d441b
github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91
github.com/matterbridge/gomatrix v0.0.0-20171224233421-78ac6a1a0f5f
github.com/matterbridge/gozulipbot v0.0.0-20180507190239-b6bb12d33544
github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea
github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18
github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61
github.com/mattermost/platform v4.6.2+incompatible
github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597 // indirect
github.com/mattn/go-isatty v0.0.0-20170216235908-dda3de49cbfc // indirect
github.com/matterbridge/mautrix-whatsapp v0.0.0-20190301210046-3539cf52ed6e
github.com/mattermost/mattermost-server v5.5.0+incompatible
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 // indirect
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9
github.com/nicksnyder/go-i18n v1.4.0 // indirect
github.com/nlopes/slack v0.4.0
github.com/nlopes/slack v0.5.0
github.com/onsi/ginkgo v1.6.0 // indirect
github.com/onsi/gomega v1.4.1 // indirect
github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83
github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect
github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e // indirect
github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320
github.com/pkg/errors v0.8.0 // indirect
github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a
github.com/russross/blackfriday v2.0.0+incompatible
github.com/rs/xid v1.2.1
github.com/russross/blackfriday v1.5.2
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 // indirect
github.com/shazow/ssh-chat v0.0.0-20171012174035-2078e1381991
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
github.com/sirupsen/logrus v1.2.0
github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296
github.com/sirupsen/logrus v1.3.0
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
github.com/spf13/afero v0.0.0-20180211162714-bbf41cb36dff // indirect
github.com/spf13/cast v1.2.0 // indirect
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec // indirect
github.com/spf13/pflag v0.0.0-20180220143236-ee5fd03fd6ac // indirect
github.com/spf13/viper v0.0.0-20171227194143-aafc9e6bc7b7
github.com/stretchr/testify v1.2.2
github.com/spf13/viper v1.3.1
github.com/stretchr/testify v1.3.0
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a // indirect
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/zfjagann/golang-ring v0.0.0-20141111230621-17637388c9f6
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect
golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 // indirect
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
golang.org/x/sys v0.0.0-20181116161606-93218def8b18 // indirect
golang.org/x/text v0.0.0-20180511172408-5c1cf69b5978 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a // indirect
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179 // indirect
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2 // indirect
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe // indirect
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 // indirect
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.9.1 // indirect
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129 // indirect
)

243
go.sum
View File

@ -1,45 +1,62 @@
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 h1:IZtuWGfzQnKnCSu+vl8WGLhpVQ5Uvy3rlSwqXSg+sQg=
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557/go.mod h1:jL0YSXMs/txjtGJ4PWrmETOk6KUHMDPMshgQZlTeB3Y=
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII=
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk=
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 h1:v/zr4ns/4sSahF9KBm4Uc933bLsEEv7LuT63CJ019yo=
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Philipp15b/go-steam v0.0.0-20161020161927-e0f3bb9566e3 h1:V4+1E1SRYUySqwOoI3ZphFADtabbF568zTHa5ix/zU0=
github.com/Philipp15b/go-steam v0.0.0-20161020161927-e0f3bb9566e3/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg=
github.com/alecthomas/log4go v0.0.0-20160307011253-e5dc62318d9b h1:1OpGXps6UOY5HtQaQcLowsV1qMWCNBzhFvK7q4fgXtc=
github.com/alecthomas/log4go v0.0.0-20160307011253-e5dc62318d9b/go.mod h1:iCVmQ9g4TfaRX5m5jq5sXY7RXYWPv9/PynM/GocbG3w=
github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E=
github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 h1:xZBoq249G9MSt+XuY7sVQzcfONJ6IQuwpCK+KAaOpnY=
github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg=
github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58/go.mod h1:YNfsMyWSs+h+PaYkxGeMVmVCX75Zj/pqdjbu12ciCYE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY=
github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/d5/tengo v1.12.1 h1:libKkDM95CsZgYs6E5eiEaM9sbcw2EzJRSkr9o5NO4s=
github.com/d5/tengo v1.12.1/go.mod h1:gsbjo7lBXzBIWBd6NQp1lRKqqiDDANqBOyhW8rTlFsY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dfordsoft/golib v0.0.0-20180313113957-2ea3495aee1d h1:rONNnZDE5CYuaSFQk+gP4GEQTXEUcyQ5p6p/dgxIHas=
github.com/dfordsoft/golib v0.0.0-20180313113957-2ea3495aee1d/go.mod h1:UGa5M2Sz/Uh13AMse4+RELKCDw7kqgqlTjeGae+7vUY=
github.com/dgrijalva/jwt-go v0.0.0-20170508165458-6c8dedd55f8a h1:MuHMeSsXbNEeUyxjB7T9P8s1+5k8OLTC/M27qsVwixM=
github.com/dgrijalva/jwt-go v0.0.0-20170508165458-6c8dedd55f8a/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec h1:JEUiu7P9smN7zgX87a2zVnnbPPickIM9Gf9OIhsIgWQ=
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec/go.mod h1:UGa5M2Sz/Uh13AMse4+RELKCDw7kqgqlTjeGae+7vUY=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180428185002-212b1541150c h1:3gMh737vMGqAkkkSfNbwjO8VRHOSaCjYRG4y9xVMEIQ=
github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180428185002-212b1541150c/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc h1:wdhDSKrkYy24mcfzuA3oYm58h0QkyXjwERCkzJDP5kA=
github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gops v0.0.0-20170319002943-62f833fc9f6c h1:MrMA1vhRTNidtgENqmsmLOIUS6ixMBOU/g10rm7IUe8=
github.com/google/gops v0.0.0-20170319002943-62f833fc9f6c/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible h1:i64CCJcSqkRIkm5OSdZQjZq84/gJsk2zNwHWIRYWlKE=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gops v0.3.5 h1:SIWvPLiYvy5vMwjxB3rVFTE4QBhUFj2KKWr3Xm7CKhw=
github.com/google/gops v0.3.5/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0=
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 h1:4EZlYQIiyecYJlUbVkFXCXHz1QPhVXcHnQKAzBTPfQo=
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4/go.mod h1:lEO7XoHJ/xNRBCxrn4h/CEB67h0kW1B0t4ooP2yrjUA=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f h1:FDM3EtwZLyhW48YRiyqjivNlNZjAObv4xt4NnJaU+NQ=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/schema v0.0.0-20170317173100-f3c80893412c h1:mORYpib1aLu3M2Oi50Z1pNTXuDJEHcoLb6oo6VdOutk=
github.com/gorilla/schema v0.0.0-20170317173100-f3c80893412c/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA=
github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po=
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb h1:1OvvPvZkn/yCQ3xBcM8y4020wdkMXPHLB4+NfoGWh4U=
github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jpillora/backoff v0.0.0-20170222002228-06c7a16c845d h1:ETeT81zgLgSNc4BWdDO2Fg9ekVItYErbNtE8mKD2pJA=
github.com/jpillora/backoff v0.0.0-20170222002228-06c7a16c845d/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/jessevdk/go-flags v1.3.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kardianos/osext v0.0.0-20170207191655-9b883c5eb462 h1:oSOOTPHkCzMeu1vJ0nHxg5+XZBdMMjNa+6NPnm8arok=
github.com/kardianos/osext v0.0.0-20170207191655-9b883c5eb462/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -47,44 +64,55 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo v0.0.0-20180219162101-7eec915044a1 h1:cOIt0LZKdfeirAfTP4VtIJuWbjVTGtd1suuPXp/J+dE=
github.com/labstack/echo v0.0.0-20180219162101-7eec915044a1/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.2.1 h1:C+I4NYknueQncqKYZQ34kHsLZJVeB5KwPUhnO0nmbpU=
github.com/labstack/gommon v0.2.1/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/lrstanley/girc v0.0.0-20180913221000-0fb5b684054e h1:RpktB2igr6nS1EN7bCvjldAEfngrM5GyAbmOa4/cafU=
github.com/lrstanley/girc v0.0.0-20180913221000-0fb5b684054e/go.mod h1:7cRs1SIBfKQ7e3Tam6GKTILSNHzR862JD0JpINaZoJk=
github.com/labstack/echo/v4 v4.0.0 h1:q1GH+caIXPP7H2StPIdzy/ez9CO0EepqYeUg6vi9SWM=
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398 h1:a40kRmhA1p2XFJ6gqXfCExSyuDDCp/U9LA8ZY27u2Lk=
github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398/go.mod h1:7cRs1SIBfKQ7e3Tam6GKTILSNHzR862JD0JpINaZoJk=
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU=
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0=
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns=
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg=
github.com/magiconair/properties v0.0.0-20180217134545-2c9e95027885 h1:HWxJJvF+QceKcql4r9PC93NtMEgEBfBxlQrZPvbcQvs=
github.com/magiconair/properties v0.0.0-20180217134545-2c9e95027885/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d h1:F+Sr+C0ojSlYQ37BLylQtSFmyQULe3jbAygcyXQ9mVs=
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d/go.mod h1:c6MxwqHD+0HvtAJjsHMIdPCiAwGiQwPRPTp69ACMg8A=
github.com/matterbridge/go-whatsapp v0.0.1-0.20190301204034-f2f1b29d441b h1:cO6Z+yj4Ivq/ay/IxSrV90oSIW/SSXWLa+XHsiLKMrw=
github.com/matterbridge/go-whatsapp v0.0.1-0.20190301204034-f2f1b29d441b/go.mod h1:dW19fYkkdUZsBAx7zv9fDh0n6NRqYIaKwB2JEBw8d0U=
github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91 h1:KzDEcy8eDbTx881giW8a6llsAck3e2bJvMyKvh1IK+k=
github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91/go.mod h1:ECDRehsR9TYTKCAsRS8/wLeOk6UUqDydw47ln7wG41Q=
github.com/matterbridge/gomatrix v0.0.0-20171224233421-78ac6a1a0f5f h1:2eKh6Qi/sJ8bXvYMoyVfQxHgR8UcCDWjOmhV1oCstMU=
github.com/matterbridge/gomatrix v0.0.0-20171224233421-78ac6a1a0f5f/go.mod h1:+jWeaaUtXQbBRdKYWfjW6JDDYiI2XXE+3NnTjW5kg8g=
github.com/matterbridge/gozulipbot v0.0.0-20180507190239-b6bb12d33544 h1:A8lLG3DAu75B5jITHs9z4JBmU6oCq1WiUNnDAmqKCZc=
github.com/matterbridge/gozulipbot v0.0.0-20180507190239-b6bb12d33544/go.mod h1:yAjnZ34DuDyPHMPHHjOsTk/FefW4JJjoMMCGt/8uuQA=
github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea h1:kaADGqpK4gGO2BpzEyJrBxq2Jc57Rsar4i2EUxcACUc=
github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea/go.mod h1:+jWeaaUtXQbBRdKYWfjW6JDDYiI2XXE+3NnTjW5kg8g=
github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18 h1:fLhwXtWGtfTgZVxHG1lcKjv+re7dRwyyuYFNu69xdho=
github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18/go.mod h1:yAjnZ34DuDyPHMPHHjOsTk/FefW4JJjoMMCGt/8uuQA=
github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61 h1:R/MgM/eUyRBQx2FiH6JVmXck8PaAuKfe2M1tWIzW7nE=
github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61/go.mod h1:iXGEotOvwI1R1SjLxRc+BF5rUORTMtE0iMZBT2lxqAU=
github.com/mattermost/platform v4.6.2+incompatible h1:9WqKNuJFIp6SDYn5wl1RF5urdhEw8d7o5tAOwT1MW0A=
github.com/mattermost/platform v4.6.2+incompatible/go.mod h1:HjGKtkQNu3HXTOykPMQckMnH11WHvNvQqDBNnVXVbfM=
github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597 h1:hGizH4aMDFFt1iOA4HNKC13lqIBoCyxIjWcAnWIy7aU=
github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.0-20170216235908-dda3de49cbfc h1:pK7tzC30erKOTfEDCYGvPZQCkmM9X5iSmmAR5m9x3Yc=
github.com/mattn/go-isatty v0.0.0-20170216235908-dda3de49cbfc/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matterbridge/mautrix-whatsapp v0.0.0-20190301210046-3539cf52ed6e h1:1NqciL8sz+0UYeFrd/UQlL8tJPhFxOBmg+a94DN2sJU=
github.com/matterbridge/mautrix-whatsapp v0.0.0-20190301210046-3539cf52ed6e/go.mod h1:DrIFGcFumRlEW5k3PJjWGKPd4+w37d3SwOxlh1ZAL+4=
github.com/mattermost/mattermost-server v5.5.0+incompatible h1:0wcLGgYtd+YImtLDPf2AOfpBHxbU4suATx+6XKw1XbU=
github.com/mattermost/mattermost-server v5.5.0+incompatible/go.mod h1:5L6MjAec+XXQwMIt791Ganu45GKsSiM+I0tLR9wUj8Y=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E=
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 h1:oKIteTqeSpenyTrOVj5zkiyCaflLa8B+CD0324otT+o=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff h1:HLGD5/9UxxfEuO9DtP8gnTmNtMxbPyhYltfxsITel8g=
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E=
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 h1:mp6tU1r0xLostUGLkTspf/9/AiHuVD7ptyXhySkDEsE=
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9/go.mod h1:A5SRAcpTemjGgIuBq6Kic2yHcoeUFWUinOAlMP/i9xo=
github.com/nicksnyder/go-i18n v1.4.0 h1:AgLl+Yq7kg5OYlzCgu9cKTZOyI4tD/NgukKqLqC8E+I=
github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
github.com/nlopes/slack v0.4.0 h1:OVnHm7lv5gGT5gkcHsZAyw++oHVFihbjWbL3UceUpiA=
github.com/nlopes/slack v0.4.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0=
github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
@ -93,75 +121,112 @@ github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83 h1:XQonH5Iv
github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83/go.mod h1:YnNlZP7l4MhyGQ4CBRwv6ohZTPrUJJZtEv4ZgADkbs4=
github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 h1:/CPgDYrfeK2LMK6xcUhvI17yO9SlpAdDIJGkhDEgO8A=
github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e h1:ZW8599OjioQsmBbkGpyruHUlRVQceYFWnJsGr4NCkiA=
github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271 h1:wQ9lVx75za6AT2kI0S9QID0uWuwTWnvcTfN+uw1F8vg=
github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271/go.mod h1:G7LufuPajuIvdt9OitkNt2qh0mmvD4bfRgRM7bhDIOA=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320 h1:YxcQy/DV+48NGv1lxx1vsWBzs6W1f1ogubkuCozxpX0=
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320/go.mod h1:G7LufuPajuIvdt9OitkNt2qh0mmvD4bfRgRM7bhDIOA=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a h1:UWKek6MK3K6/TpbsFcv+8rrO6rSc6KKSp2FbMOHWsq4=
github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 h1:Lx3BlDGFElJt4u/zKc9A3BuGYbQAGlEFyPuUA3jeMD0=
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI=
github.com/shazow/ssh-chat v0.0.0-20171012174035-2078e1381991 h1:PQiUTDzUC5EUh0vNurK7KQS22zlKqLLOFn+K9nJXDQQ=
github.com/shazow/ssh-chat v0.0.0-20171012174035-2078e1381991/go.mod h1:KwtnpMClmrXsHCKTbRui5xBUNt17n1GGrGhdiw2KcoY=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296 h1:8RLq547MSVc6vhOuCl4Ca0TsAQknj6NX6ZLSZ3+xmio=
github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296/go.mod h1:1GLXsL4esywkpNId3v4QWuMf3THtWGitWvtQ/L3aSA4=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 h1:lXQ+j+KwZcbwrbgU0Rp4Eglg3EJLHbuZU3BbOqAGBmg=
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/spf13/afero v0.0.0-20180211162714-bbf41cb36dff h1:HLvGWId7M56TfuxTeZ6aoiTAcrWO5Mnq/ArwVRgV62I=
github.com/spf13/afero v0.0.0-20180211162714-bbf41cb36dff/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig=
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v0.0.0-20180220143236-ee5fd03fd6ac h1:+uzyQ0TQ3aKorQxsOjcDDgE7CuUXwpkKnK19LULQALQ=
github.com/spf13/pflag v0.0.0-20180220143236-ee5fd03fd6ac/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v0.0.0-20171227194143-aafc9e6bc7b7 h1:Wj4cg2M6Um7j1N7yD/mxsdy1/wrsdjzVha2eWdOhti8=
github.com/spf13/viper v0.0.0-20171227194143-aafc9e6bc7b7/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38=
github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a h1:AOcehBWpFhYPYw0ioDTppQzgI8pAAahVCiMSKTp9rbo=
github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/zfjagann/golang-ring v0.0.0-20141111230621-17637388c9f6 h1:/WULP+6asFz569UbOwg87f3iDT7T+GF5/vjLmL51Pdk=
github.com/zfjagann/golang-ring v0.0.0-20141111230621-17637388c9f6/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447 h1:CHgPZh8bFkZmislPrr/0gd7MciDAX+JJB70A2/5Lvmo=
github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU=
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a h1:Ax7kdHNICZiIeFpmevmaEWb0Ae3BUj3zCTKhZHZ+zd0=
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a/go.mod h1:JT4uoTz0tfPoyVH88GZoWDNm5NHJI2VbUW+eyPClueI=
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179 h1:rbON2KwBnWuFMlSHM8LELLlwroDRZw6xv0e6il6e5dk=
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8=
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f h1:jwXy/CsM4xS2aoiF2fHAlukmInWhd2TlWB+HDCyvzKc=
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f/go.mod h1:SIHlEr9462fpIfTrVWf3GqQDxnA65Vm3BMMsUtuA6W0=
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2 h1:wD/sPUgx2QJFPTyXZpJnLaROolfeKuruh06U4pRV0WY=
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2/go.mod h1:wQk4rLkWrdOPjUAtqJRJ10hIlseLSVYWP95PLrjDF9s=
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe h1:5kUPFAF52umOUPH12MuNUmyVTseJRNBftDl/KfsvX3I=
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe/go.mod h1:P9LSM1KVzrIstFgUaveuwiAm8PK5VTB3yJEU8kqlbrU=
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 h1:uPZaMiz6Sz0PZs3IZJWpU5qHKGNy///1pacZC9txiUI=
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638/go.mod h1:EGRJaqe2eO9XGmFtQCvV3Lm9NLico3UhFwUpCG/+mVU=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 h1:BkNcmLtAVeWe9h5k0jt24CQgaG5vb4x/doFbAiEC/Ho=
golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f h1:qWFY9ZxP3tfI37wYIs/MnIAqK0vlXp1xnYEa5HxFSSY=
golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9 h1:+vH8qNweCrORN49012OX3h0oWEXO3p+rRnpAGQinddk=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116161606-93218def8b18 h1:Wh+XCfg3kNpjhdq2LXrsiOProjtQZKme5XUx7VcxwAw=
golang.org/x/sys v0.0.0-20181116161606-93218def8b18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.0.0-20180511172408-5c1cf69b5978 h1:WNm0tmiuBMW4FJRuXKWOqaQfmKptHs0n8nTCyG0ayjc=
golang.org/x/text v0.0.0-20180511172408-5c1cf69b5978/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222171317-cd391775e71e h1:oF7qaQxUH6KzFdKN4ww7NpPdo53SZi4UlcksLrb2y/o=
golang.org/x/sys v0.0.0-20190222171317-cd391775e71e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129 h1:RBgb9aPUbZ9nu66ecQNIBNsA7j3mB5h8PNDIfhPjaJg=
gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/maulogger/v2 v2.0.0/go.mod h1:Hbbkq3NV6jvJodByZu1mgEF3fpT7Kz9z0MjEZ3/BusI=
maunium.net/go/mautrix v0.1.0-alpha.3/go.mod h1:GTVu6WDHR+98DKOrYetWsXorvUeKQV3jsSWO6ScbuFI=
maunium.net/go/mautrix-appservice v0.1.0-alpha.3/go.mod h1:wOnWOIuprYad7ly12rHIo3JLCPh4jwvx1prVrAB9RhM=

View File

@ -38,7 +38,7 @@ type Config struct {
func New(url string, config Config) *Client {
c := &Client{In: make(chan Message), Config: config}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}, //nolint:gosec
}
c.httpclient = &http.Client{Transport: tr}
_, _, err := net.SplitHostPort(c.BindAddress)

View File

@ -8,51 +8,78 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway"
"github.com/42wim/matterbridge/gateway/bridgemap"
"github.com/google/gops/agent"
prefixed "github.com/matterbridge/logrus-prefixed-formatter"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
)
var (
version = "1.12.1-dev"
version = "1.14.1"
githash string
flagConfig = flag.String("conf", "matterbridge.toml", "config file")
flagDebug = flag.Bool("debug", false, "enable debug")
flagVersion = flag.Bool("version", false, "show version")
flagGops = flag.Bool("gops", false, "enable gops agent")
)
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")
flagDebug := flag.Bool("debug", false, "enable debug")
flagVersion := flag.Bool("version", false, "show version")
flagGops := flag.Bool("gops", false, "enable gops agent")
flag.Parse()
if *flagGops {
agent.Listen(&agent.Options{})
defer agent.Close()
}
if *flagVersion {
fmt.Printf("version: %s %s\n", version, githash)
return
}
if *flagDebug || os.Getenv("DEBUG") == "1" {
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false, ForceFormatting: true})
flog.Info("Enabling debug")
log.SetLevel(log.DebugLevel)
rootLogger := setupLogger()
logger := rootLogger.WithFields(logrus.Fields{"prefix": "main"})
if *flagGops {
if err := agent.Listen(agent.Options{}); err != nil {
logger.Errorf("Failed to start gops agent: %#v", err)
} else {
defer agent.Close()
}
}
flog.Printf("Running version %s %s", version, githash)
logger.Printf("Running version %s %s", version, githash)
if strings.Contains(version, "-dev") {
flog.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
logger.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
}
cfg := config.NewConfig(*flagConfig)
cfg := config.NewConfig(rootLogger, *flagConfig)
cfg.BridgeValues().General.Debug = *flagDebug
r, err := gateway.NewRouter(cfg)
r, err := gateway.NewRouter(rootLogger, cfg, bridgemap.FullMap)
if err != nil {
flog.Fatalf("Starting gateway failed: %s", err)
logger.Fatalf("Starting gateway failed: %s", err)
}
err = r.Start()
if err != nil {
flog.Fatalf("Starting gateway failed: %s", err)
if err = r.Start(); err != nil {
logger.Fatalf("Starting gateway failed: %s", err)
}
flog.Printf("Gateway(s) started succesfully. Now relaying messages")
logger.Printf("Gateway(s) started succesfully. Now relaying messages")
select {}
}
func setupLogger() *logrus.Logger {
logger := &logrus.Logger{
Out: os.Stdout,
Formatter: &prefixed.TextFormatter{
PrefixPadding: 13,
DisableColors: true,
FullTimestamp: true,
},
Level: logrus.InfoLevel,
}
if *flagDebug || os.Getenv("DEBUG") == "1" {
logger.Formatter = &prefixed.TextFormatter{
PrefixPadding: 13,
DisableColors: true,
FullTimestamp: false,
ForceFormatting: true,
}
logger.Level = logrus.DebugLevel
logger.WithFields(logrus.Fields{"prefix": "main"}).Info("Enabling debug logging.")
}
return logger
}

View File

@ -96,7 +96,13 @@ RejoinDelay=0
#Only works in IRC right now.
ColorNicks=false
#RunCommands allows you to send RAW irc commands after connection
#Array of strings
#OPTIONAL (default empty)
RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"]
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
@ -124,6 +130,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -134,7 +151,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -191,6 +208,7 @@ SkipTLSVerify=true
## Settings below can be reloaded by editing the file
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
@ -218,6 +236,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#OPTIONAL (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -227,7 +256,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -271,6 +300,7 @@ Nick="yourlogin"
## Settings below can be reloaded by editing the file
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
@ -298,6 +328,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -307,7 +348,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -411,6 +452,7 @@ EditDisable=false
EditSuffix=" (edited)"
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
@ -438,6 +480,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -447,7 +500,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -486,6 +539,7 @@ Token="Yourtokenhere"
## Settings below can be reloaded by editing the file
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
@ -513,6 +567,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -522,7 +587,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -552,6 +617,10 @@ ShowTopicChange=false
#REQUIRED (when not using webhooks)
Token="yourslacktoken"
#Extra slack specific debug info, warning this generates a lot of output.
#OPTIONAL (default false)
Debug="false"
#### Settings for webhook matterbridge.
#NOT RECOMMENDED TO USE INCOMING/OUTGOING WEBHOOK. USE SLACK API
#AND DEDICATED BOT USER WHEN POSSIBLE!
@ -604,6 +673,7 @@ EditSuffix=" (edited)"
PrefixMessagesWithNick=false
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
@ -631,6 +701,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -640,7 +721,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -662,6 +743,8 @@ ShowTopicChange=false
#Opportunistically preserve threaded replies between Slack channels.
#This only works if the parent message is still in the cache.
#Cache is flushed between restarts.
#Note: Not currently working on gateways with mixed bridges of
# both slack and slack-legacy type. Context in issue #624.
#OPTIONAL (default false)
PreserveThreading=false
@ -697,10 +780,14 @@ Server="yourservername"
#OPTIONAL (default false)
ShowEmbeds=false
#Shows the username (minus the discriminator) instead of the server nickname
#Shows the username instead of the server nickname
#OPTIONAL (default false)
UseUserName=false
#Show #xxxx discriminator with UseUserName
#OPTIONAL (default false)
UseDiscriminator=false
#Specify WebhookURL. If given, will relay messages using the Webhook, which gives a better look to messages.
#This only works if you have one discord channel, if you have multiple discord channels you'll have to specify it in the gateway config
#OPTIONAL (default empty)
@ -715,6 +802,7 @@ EditDisable=false
EditSuffix=" (edited)"
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
@ -742,6 +830,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -751,7 +850,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -760,11 +859,16 @@ ShowJoinPart=false
#OPTIONAL (default false)
StripNick=false
#Enable to show topic changes from other bridges
#Enable to show topic/purpose changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
#Enable to sync topic/purpose changes from other bridges
#Only works syncing topic changes from slack bridge for now
#OPTIONAL (default false)
SyncTopic=false
###################################################################
#telegram section
###################################################################
@ -809,6 +913,11 @@ QuoteDisable=false
#OPTIONAL (default "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})")
QuoteFormat="{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
#Convert WebP images to PNG before upload.
#https://github.com/42wim/matterbridge/issues/398
#OPTIONAL (default false)
MediaConvertWebPToPNG=false
#Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=false
@ -818,6 +927,7 @@ EditDisable=false
EditSuffix=" (edited)"
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
@ -845,6 +955,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -858,7 +979,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -881,6 +1002,22 @@ ShowTopicChange=false
#REQUIRED
[rocketchat.rockme]
#The rocketchat hostname. (prefix it with http or https)
#REQUIRED (when not using webhooks)
Server="https://yourrocketchatserver.domain.com:443"
#login/pass of your bot.
#login needs to be the login with email address! user@domain.com
#Use a dedicated user for this and not your own!
#REQUIRED (when not using webhooks)
Login="yourlogin@domain.com"
Password="yourpass"
#### Settings for webhook matterbridge.
#USE DEDICATED BOT USER WHEN POSSIBLE! This allows you to use advanced features like message editing/deleting and uploads
#You don't need to configure this, if you have configured the settings
#above.
#Url is your incoming webhook url as specified in rocketchat
#Read #https://rocket.chat/docs/administrator-guides/integrations/#how-to-create-a-new-incoming-webhook
#See administration - integrations - new integration - incoming webhook
@ -905,6 +1042,8 @@ NoTLS=false
#OPTIONAL (default false)
SkipTLSVerify=true
#### End settings for webhook matterbridge.
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
@ -912,10 +1051,13 @@ SkipTLSVerify=true
#Useful if username overrides for incoming webhooks isn't enabled on the
#rocketchat server. If you set PrefixMessagesWithNick to true, each message
#from bridge to rocketchat will by default be prefixed by the RemoteNickFormat setting. i
#if you're using login/pass you can better enable because of this bug:
#https://github.com/RocketChat/Rocket.Chat/issues/7549
#OPTIONAL (default false)
PrefixMessagesWithNick=false
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
@ -943,6 +1085,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -952,7 +1105,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -1002,6 +1155,7 @@ NoHomeServerSuffix=false
PrefixMessagesWithNick=false
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
@ -1029,6 +1183,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -1038,7 +1203,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -1082,6 +1247,7 @@ Authcode="ABCE12"
PrefixMessagesWithNick=false
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
@ -1109,6 +1275,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -1118,7 +1295,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -1132,10 +1309,45 @@ StripNick=false
#OPTIONAL (default false)
ShowTopicChange=false
###################################################################
#zulip section
#
# WhatsApp
#
###################################################################
[whatsapp.bridge]
# Number you will use as a relay bot. Tip: Get some disposable sim card, don't rely on your own number.
Number="+48111222333"
# First time that you login you will need to scan QR code, then credentials willl be saved in a session file
# If you won't set SessionFile then you will need to scan QR code on every restart
# optional (by default the session is stored only in memory, till restarting matterbridge)
SessionFile="session-48111222333.gob"
# If your terminal is white we need to invert QR code in order for it to be scanned properly
# optional (default false)
QrOnWhiteTerminal=true
# Messages will be seen by other WhatsApp contacts as coming from the bridge. Original nick will be part of the message.
RemoteNickFormat="@{NICK}: "
# extra label that can be used in the RemoteNickFormat
# optional (default empty)
Label="Organization"
###################################################################
#
# zulip
#
###################################################################
[zulip]
#You can configure multiple servers "[zulip.name]" or "[zulip.name2]"
#In this example we use [zulip.streamchat]
#REQUIRED
@ -1154,14 +1366,11 @@ Login="yourbot-bot@yourserver.zulipchat.com"
#REQUIRED
Server="https://yourserver.zulipchat.com"
#Topic of the messages matterbridge will use
#OPTIONAL (default "matterbridge")
Topic="matterbridge"
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="spammer1 spammer2"
@ -1189,6 +1398,17 @@ ReplaceMessages=[ ["cat","dog"] ]
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
@ -1198,7 +1418,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
@ -1303,6 +1523,35 @@ MediaDownloadSize=1000000
#OPTIONAL (default empty)
MediaDownloadBlacklist=[".html$",".htm$"]
#IgnoreFailureOnStart allows you to ignore failing bridges on startup.
#Matterbridge will disable the failed bridge and continue with the other ones.
#Context: https://github.com/42wim/matterbridge/issues/455
#OPTIONAL (default false)
IgnoreFailureOnStart=false
#TengoModifyMessage allows you to specify the location of a tengo (https://github.com/d5/tengo/) script.
#This script will receive every incoming message and can be used to modify the Username and the Text of that message.
#The script will have the following global variables:
#to modify: msgUsername and msgText
#to read: msgChannel and msgAccount
#
#The script is reloaded on every message, so you can modify the script on the fly.
#
#Example script can be found in https://github.com/42wim/matterbridge/tree/master/gateway/bench.tengo
#and https://github.com/42wim/matterbridge/tree/master/contrib/example.tengo
#
#The example below will check if the text contains blah and if so, it'll replace the text and the username of that message.
#text := import("text")
#if text.re_match("blah",msgText) {
# msgText="replaced by this"
# msgUsername="fakeuser"
#}
#More information about tengo on: https://github.com/d5/tengo/blob/master/docs/tutorial.md and
#https://github.com/d5/tengo/blob/master/docs/stdlib.md
#OPTIONAL (default empty)
TengoModifyMessage="example.tengo"
###################################################################
#Gateway configuration
###################################################################
@ -1324,36 +1573,41 @@ name="gateway1"
##OPTIONAL (default false)
enable=true
#[[gateway.in]] specifies the account and channels we will receive messages from.
#The following example bridges between mattermost and irc
# [[gateway.in]] specifies the account and channels we will receive messages from.
# The following example bridges between mattermost and irc
[[gateway.in]]
#account specified above
#REQUIRED
# account specified above
# REQUIRED
account="irc.freenode"
#channel to connect on that account
#How to specify them for the different bridges:
# channel to connect on that account
# How to specify them for the different bridges:
#
#irc - #channel (# is required) (this needs to be lowercase!)
#mattermost - channel (the channel name as seen in the URL, not the displayname)
#gitter - username/room
#xmpp - channel
#slack - channel (without the #)
# - ID:C123456 (where C123456 is the channel ID) does not work with webhook
#discord - channel (without the #)
# - ID:123456789 (where 123456789 is the channel ID)
# irc - #channel (# is required) (this needs to be lowercase!)
# mattermost - channel (the channel name as seen in the URL, not the displayname)
# gitter - username/room
# xmpp - channel
# slack - channel (without the #)
# - ID:C123456 (where C123456 is the channel ID) does not work with webhook
# discord - channel (without the #)
# - ID:123456789 (where 123456789 is the channel ID)
# (https://github.com/42wim/matterbridge/issues/57)
#telegram - chatid (a large negative number, eg -123456789)
# telegram - chatid (a large negative number, eg -123456789)
# see (https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau)
#hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
#rocketchat - #channel (# is required (also needed for private channels!)
#matrix - #channel:server (eg #yourchannel:matrix.org)
# - encrypted rooms are not supported in matrix
#steam - chatid (a large number).
# hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
# rocketchat - #channel (# is required (also needed for private channels!)
# matrix - #channel:server (eg #yourchannel:matrix.org)
# - encrypted rooms are not supported in matrix
# steam - chatid (a large number).
# The number in the URL when you click "enter chat room" in the browser
#zulip - stream (without the #)
# whatsapp - 48111222333-123455678999@g.us A unique group JID;
# if you specify an empty string bridge will list all the possibilities
# - "Group Name" if you specify a group name the bridge will hint its JID to specify
# as group names might change in time and contain weird emoticons
# zulip - stream/topic:topicname (without the #)
#
#REQUIRED
# REQUIRED
channel="#testing"
#OPTIONAL - only used for IRC and XMPP protocols at the moment
@ -1389,7 +1643,11 @@ enable=true
#OPTIONAL - webhookurl only works for discord (it needs a different URL for each cahnnel)
[gateway.inout.options]
webhookurl=""https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y"
webhookurl="https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y"
[[gateway.inout]]
account="zulip.streamchat"
channel="general/topic:mytopic"
#API example
#[[gateway.inout]]

207
matterclient/channels.go Normal file
View File

@ -0,0 +1,207 @@
package matterclient
import (
"errors"
"strings"
"github.com/mattermost/mattermost-server/model"
)
// GetChannels returns all channels we're members off
func (m *MMClient) GetChannels() []*model.Channel {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
// our primary team channels first
channels = append(channels, m.Team.Channels...)
for _, t := range m.OtherTeams {
if t.Id != m.Team.Id {
channels = append(channels, t.Channels...)
}
}
return channels
}
func (m *MMClient) GetChannelHeader(channelId string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == channelId {
return channel.Header
}
}
}
return ""
}
func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
if teamId != "" {
return m.getChannelIdTeam(name, teamId)
}
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Type == model.CHANNEL_GROUP {
res := strings.Replace(channel.DisplayName, ", ", "-", -1)
res = strings.Replace(res, " ", "_", -1)
if res == name {
return channel.Id
}
}
}
}
return ""
}
func (m *MMClient) getChannelIdTeam(name string, teamId string) string { //nolint:golint
for _, t := range m.OtherTeams {
if t.Id == teamId {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Name == name {
return channel.Id
}
}
}
}
return ""
}
func (m *MMClient) GetChannelName(channelId string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if t == nil {
continue
}
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == channelId {
if channel.Type == model.CHANNEL_GROUP {
res := strings.Replace(channel.DisplayName, ", ", "-", -1)
res = strings.Replace(res, " ", "_", -1)
return res
}
return channel.Name
}
}
}
return ""
}
func (m *MMClient) GetChannelTeamId(id string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
for _, t := range append(m.OtherTeams, m.Team) {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == id {
return channel.TeamId
}
}
}
return ""
}
func (m *MMClient) GetLastViewedAt(channelId string) int64 { //nolint:golint
m.RLock()
defer m.RUnlock()
res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "")
if resp.Error != nil {
return model.GetMillis()
}
return res.LastViewedAt
}
// GetMoreChannels returns existing channels where we're not a member off.
func (m *MMClient) GetMoreChannels() []*model.Channel {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.MoreChannels...)
}
return channels
}
// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId).
func (m *MMClient) GetTeamFromChannel(channelId string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.Channels...)
if t.MoreChannels != nil {
channels = append(channels, t.MoreChannels...)
}
for _, c := range channels {
if c.Id == channelId {
if c.Type == model.CHANNEL_GROUP {
return "G"
}
return t.Id
}
}
channels = nil
}
return ""
}
func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint
m.RLock()
defer m.RUnlock()
for _, c := range m.Team.Channels {
if c.Id == channelId {
m.logger.Debug("Not joining ", channelId, " already joined.")
return nil
}
}
m.logger.Debug("Joining ", channelId)
_, resp := m.Client.AddChannelMember(channelId, m.User.Id)
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) UpdateChannels() error {
mmchannels, resp := m.Client.GetChannelsForTeamForUser(m.Team.Id, m.User.Id, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
m.Lock()
m.Team.Channels = mmchannels
m.Unlock()
mmchannels, resp = m.Client.GetPublicChannelsForTeam(m.Team.Id, 0, 5000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
m.Lock()
m.Team.MoreChannels = mmchannels
m.Unlock()
return nil
}
func (m *MMClient) UpdateChannelHeader(channelId string, header string) { //nolint:golint
channel := &model.Channel{Id: channelId, Header: header}
m.logger.Debugf("updating channelheader %#v, %#v", channelId, header)
_, resp := m.Client.UpdateChannel(channel)
if resp.Error != nil {
m.logger.Error(resp.Error)
}
}
func (m *MMClient) UpdateLastViewed(channelId string) error { //nolint:golint
m.logger.Debugf("posting lastview %#v", channelId)
view := &model.ChannelView{ChannelId: channelId}
_, resp := m.Client.ViewChannel(m.User.Id, view)
if resp.Error != nil {
m.logger.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error)
return resp.Error
}
return nil
}

282
matterclient/helpers.go Normal file
View File

@ -0,0 +1,282 @@
package matterclient
import (
"crypto/md5" //nolint:gosec
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/jpillora/backoff"
"github.com/mattermost/mattermost-server/model"
)
func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error {
var resp *model.Response
var appErr *model.AppError
var logmsg = "trying login"
var err error
for {
m.logger.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
if m.Credentials.Token != "" {
resp, err = m.doLoginToken()
if err != nil {
return err
}
} else {
m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
}
appErr = resp.Error
if appErr != nil {
d := b.Duration()
m.logger.Debug(appErr.DetailedError)
if firstConnection {
if appErr.Message == "" {
return errors.New(appErr.DetailedError)
}
return errors.New(appErr.Message)
}
m.logger.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
time.Sleep(d)
logmsg = "retrying login"
continue
}
break
}
// reset timer
b.Reset()
return nil
}
func (m *MMClient) doLoginToken() (*model.Response, error) {
var resp *model.Response
var logmsg = "trying login"
m.Client.AuthType = model.HEADER_BEARER
m.Client.AuthToken = m.Credentials.Token
if m.Credentials.CookieToken {
m.logger.Debugf(logmsg + " with cookie (MMAUTH) token")
m.Client.HttpClient.Jar = m.createCookieJar(m.Credentials.Token)
} else {
m.logger.Debugf(logmsg + " with personal token")
}
m.User, resp = m.Client.GetMe("")
if resp.Error != nil {
return resp, resp.Error
}
if m.User == nil {
m.logger.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
return resp, errors.New("invalid token")
}
return resp, nil
}
func (m *MMClient) handleLoginToken() error {
switch {
case strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN):
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
if len(token) != 2 {
return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
}
m.Credentials.Token = token[1]
m.Credentials.CookieToken = true
case strings.Contains(m.Credentials.Pass, "token="):
token := strings.Split(m.Credentials.Pass, "token=")
if len(token) != 2 {
return errors.New("incorrect personal token. valid input is token=yourtoken")
}
m.Credentials.Token = token[1]
}
return nil
}
func (m *MMClient) initClient(firstConnection bool, b *backoff.Backoff) error {
uriScheme := "https://"
if m.NoTLS {
uriScheme = "http://"
}
// login to mattermost
m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
Proxy: http.ProxyFromEnvironment,
}
m.Client.HttpClient.Timeout = time.Second * 10
// handle MMAUTHTOKEN and personal token
if err := m.handleLoginToken(); err != nil {
return err
}
// check if server alive, retry until
if err := m.serverAlive(firstConnection, b); err != nil {
return err
}
return nil
}
// initialize user and teams
func (m *MMClient) initUser() error {
m.Lock()
defer m.Unlock()
// we only load all team data on initial login.
// all other updates are for channels from our (primary) team only.
//m.logger.Debug("initUser(): loading all team data")
teams, resp := m.Client.GetTeamsForUser(m.User.Id, "")
if resp.Error != nil {
return resp.Error
}
for _, team := range teams {
mmusers, resp := m.Client.GetUsersInTeam(team.Id, 0, 50000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
usermap := make(map[string]*model.User)
for _, user := range mmusers {
usermap[user.Id] = user
}
t := &Team{Team: team, Users: usermap, Id: team.Id}
mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, "")
if resp.Error != nil {
return resp.Error
}
t.Channels = mmchannels
mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "")
if resp.Error != nil {
return resp.Error
}
t.MoreChannels = mmchannels
m.OtherTeams = append(m.OtherTeams, t)
if team.Name == m.Credentials.Team {
m.Team = t
m.logger.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id)
}
// add all users
for k, v := range t.Users {
m.Users[k] = v
}
}
return nil
}
func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error {
defer b.Reset()
for {
d := b.Duration()
// bogus call to get the serverversion
_, resp := m.Client.Logout()
if resp.Error != nil {
return fmt.Errorf("%#v", resp.Error.Error())
}
if firstConnection && !supportedVersion(resp.ServerVersion) {
return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion)
}
m.ServerVersion = resp.ServerVersion
if m.ServerVersion == "" {
m.logger.Debugf("Server not up yet, reconnecting in %s", d)
time.Sleep(d)
} else {
m.logger.Infof("Found version %s", m.ServerVersion)
return nil
}
}
}
func (m *MMClient) wsConnect() {
b := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
m.WsConnected = false
wsScheme := "wss://"
if m.NoTLS {
wsScheme = "ws://"
}
// setup websocket connection
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket"
header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
m.logger.Debugf("WsClient: making connection: %s", wsurl)
for {
wsDialer := &websocket.Dialer{
TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
Proxy: http.ProxyFromEnvironment,
}
var err error
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
if err != nil {
d := b.Duration()
m.logger.Debugf("WSS: %s, reconnecting in %s", err, d)
time.Sleep(d)
continue
}
break
}
m.logger.Debug("WsClient: connected")
m.WsSequence = 1
m.WsPingChan = make(chan *model.WebSocketResponse)
// only start to parse WS messages when login is completely done
m.WsConnected = true
}
func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
var cookies []*http.Cookie
jar, _ := cookiejar.New(nil)
firstCookie := &http.Cookie{
Name: "MMAUTHTOKEN",
Value: token,
Path: "/",
Domain: m.Credentials.Server,
}
cookies = append(cookies, firstCookie)
cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
jar.SetCookies(cookieURL, cookies)
return jar
}
func (m *MMClient) checkAlive() error {
// check if session still is valid
_, resp := m.Client.GetMe("")
if resp.Error != nil {
return resp.Error
}
m.logger.Debug("WS PING")
return m.sendWSRequest("ping", nil)
}
func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error {
req := &model.WebSocketRequest{}
req.Seq = m.WsSequence
req.Action = action
req.Data = data
m.WsSequence++
m.logger.Debugf("sendWsRequest %#v", req)
return m.WsClient.WriteJSON(req)
}
func supportedVersion(version string) bool {
if strings.HasPrefix(version, "3.8.0") ||
strings.HasPrefix(version, "3.9.0") ||
strings.HasPrefix(version, "3.10.0") ||
strings.HasPrefix(version, "4.") ||
strings.HasPrefix(version, "5.") {
return true
}
return false
}
func digestString(s string) string {
return fmt.Sprintf("%x", md5.Sum([]byte(s))) //nolint:gosec
}

File diff suppressed because it is too large Load Diff

207
matterclient/messages.go Normal file
View File

@ -0,0 +1,207 @@
package matterclient
import (
"strings"
"github.com/mattermost/mattermost-server/model"
)
func (m *MMClient) parseActionPost(rmsg *Message) {
// add post to cache, if it already exists don't relay this again.
// this should fix reposts
if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok {
m.logger.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string))
rmsg.Text = ""
return
}
data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string)))
// we don't have the user, refresh the userlist
if m.GetUser(data.UserId) == nil {
m.logger.Infof("User '%v' is not known, ignoring message '%#v'",
data.UserId, data)
return
}
rmsg.Username = m.GetUserName(data.UserId)
rmsg.Channel = m.GetChannelName(data.ChannelId)
rmsg.UserID = data.UserId
rmsg.Type = data.Type
teamid, _ := rmsg.Raw.Data["team_id"].(string)
// edit messsages have no team_id for some reason
if teamid == "" {
// we can find the team_id from the channelid
teamid = m.GetChannelTeamId(data.ChannelId)
rmsg.Raw.Data["team_id"] = teamid
}
if teamid != "" {
rmsg.Team = m.GetTeamName(teamid)
}
// direct message
if rmsg.Raw.Data["channel_type"] == "D" {
rmsg.Channel = m.GetUser(data.UserId).Username
}
rmsg.Text = data.Message
rmsg.Post = data
}
func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Event {
case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED:
m.parseActionPost(rmsg)
case "user_updated":
user := rmsg.Raw.Data["user"].(map[string]interface{})
if _, ok := user["id"].(string); ok {
m.UpdateUser(user["id"].(string))
}
case "group_added":
if err := m.UpdateChannels(); err != nil {
m.logger.Errorf("failed to update channels: %#v", err)
}
/*
case model.ACTION_USER_REMOVED:
m.handleWsActionUserRemoved(&rmsg)
case model.ACTION_USER_ADDED:
m.handleWsActionUserAdded(&rmsg)
*/
}
}
func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) {
if rmsg.Data != nil {
// ping reply
if rmsg.Data["text"].(string) == "pong" {
m.WsPingChan <- &rmsg
}
}
}
func (m *MMClient) DeleteMessage(postId string) error { //nolint:golint
_, resp := m.Client.DeletePost(postId)
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint
post := &model.Post{Message: text}
res, resp := m.Client.UpdatePost(postId, post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) GetFileLinks(filenames []string) []string {
uriScheme := "https://"
if m.NoTLS {
uriScheme = "http://"
}
var output []string
for _, f := range filenames {
res, resp := m.Client.GetFileLink(f)
if resp.Error != nil {
// public links is probably disabled, create the link ourselves
output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V4+"/files/"+f)
continue
}
output = append(output, res)
}
return output
}
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList { //nolint:golint
res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "")
if resp.Error != nil {
return nil
}
return res
}
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList { //nolint:golint
res, resp := m.Client.GetPostsSince(channelId, time)
if resp.Error != nil {
return nil
}
return res
}
func (m *MMClient) GetPublicLink(filename string) string {
res, resp := m.Client.GetFileLink(filename)
if resp.Error != nil {
return ""
}
return res
}
func (m *MMClient) GetPublicLinks(filenames []string) []string {
var output []string
for _, f := range filenames {
res, resp := m.Client.GetFileLink(f)
if resp.Error != nil {
continue
}
output = append(output, res)
}
return output
}
func (m *MMClient) PostMessage(channelId string, text string, rootId string) (string, error) { //nolint:golint
post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId}
res, resp := m.Client.CreatePost(post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) PostMessageWithFiles(channelId string, text string, rootId string, fileIds []string) (string, error) { //nolint:golint
post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId, FileIds: fileIds}
res, resp := m.Client.CreatePost(post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) SearchPosts(query string) *model.PostList {
res, resp := m.Client.SearchPosts(m.Team.Id, query, false)
if resp.Error != nil {
return nil
}
return res
}
// SendDirectMessage sends a direct message to specified user
func (m *MMClient) SendDirectMessage(toUserId string, msg string, rootId string) { //nolint:golint
m.SendDirectMessageProps(toUserId, msg, rootId, nil)
}
func (m *MMClient) SendDirectMessageProps(toUserId string, msg string, rootId string, props map[string]interface{}) { //nolint:golint
m.logger.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
// create DM channel (only happens on first message)
_, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId)
if resp.Error != nil {
m.logger.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error)
return
}
channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
// update our channels
if err := m.UpdateChannels(); err != nil {
m.logger.Errorf("failed to update channels: %#v", err)
}
// build & send the message
msg = strings.Replace(msg, "\r", "", -1)
post := &model.Post{ChannelId: m.GetChannelId(channelName, m.Team.Id), Message: msg, RootId: rootId, Props: props}
m.Client.CreatePost(post)
}
func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) { //nolint:golint
f, resp := m.Client.UploadFile(data, channelId, filename)
if resp.Error != nil {
return "", resp.Error
}
return f.FileInfos[0].Id, nil
}

154
matterclient/users.go Normal file
View File

@ -0,0 +1,154 @@
package matterclient
import (
"errors"
"github.com/mattermost/mattermost-server/model"
)
func (m *MMClient) GetNickName(userId string) string { //nolint:golint
user := m.GetUser(userId)
if user != nil {
return user.Nickname
}
return ""
}
func (m *MMClient) GetStatus(userId string) string { //nolint:golint
res, resp := m.Client.GetUserStatus(userId, "")
if resp.Error != nil {
return ""
}
if res.Status == model.STATUS_AWAY {
return "away"
}
if res.Status == model.STATUS_ONLINE {
return "online"
}
return "offline"
}
func (m *MMClient) GetStatuses() map[string]string {
var ids []string
statuses := make(map[string]string)
for id := range m.Users {
ids = append(ids, id)
}
res, resp := m.Client.GetUsersStatusesByIds(ids)
if resp.Error != nil {
return statuses
}
for _, status := range res {
statuses[status.UserId] = "offline"
if status.Status == model.STATUS_AWAY {
statuses[status.UserId] = "away"
}
if status.Status == model.STATUS_ONLINE {
statuses[status.UserId] = "online"
}
}
return statuses
}
func (m *MMClient) GetTeamId() string { //nolint:golint
return m.Team.Id
}
// GetTeamName returns the name of the specified teamId
func (m *MMClient) GetTeamName(teamId string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if t.Id == teamId {
return t.Team.Name
}
}
return ""
}
func (m *MMClient) GetUser(userId string) *model.User { //nolint:golint
m.Lock()
defer m.Unlock()
_, ok := m.Users[userId]
if !ok {
res, resp := m.Client.GetUser(userId, "")
if resp.Error != nil {
return nil
}
m.Users[userId] = res
}
return m.Users[userId]
}
func (m *MMClient) GetUserName(userId string) string { //nolint:golint
user := m.GetUser(userId)
if user != nil {
return user.Username
}
return ""
}
func (m *MMClient) GetUsers() map[string]*model.User {
users := make(map[string]*model.User)
m.RLock()
defer m.RUnlock()
for k, v := range m.Users {
users[k] = v
}
return users
}
func (m *MMClient) UpdateUsers() error {
mmusers, resp := m.Client.GetUsers(0, 50000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
m.Lock()
for _, user := range mmusers {
m.Users[user.Id] = user
}
m.Unlock()
return nil
}
func (m *MMClient) UpdateUserNick(nick string) error {
user := m.User
user.Nickname = nick
_, resp := m.Client.UpdateUser(user)
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) UsernamesInChannel(channelId string) []string { //nolint:golint
res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "")
if resp.Error != nil {
m.logger.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error)
return []string{}
}
allusers := m.GetUsers()
result := []string{}
for _, member := range *res {
result = append(result, allusers[member.UserId].Nickname)
}
return result
}
func (m *MMClient) UpdateStatus(userId string, status string) error { //nolint:golint
_, resp := m.Client.UpdateUserStatus(userId, &model.Status{Status: status})
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) UpdateUser(userId string) { //nolint:golint
m.Lock()
defer m.Unlock()
res, resp := m.Client.GetUser(userId, "")
if resp.Error != nil {
return
}
m.Users[userId] = res
}

View File

@ -71,7 +71,7 @@ type Config struct {
func New(url string, config Config) *Client {
c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}, //nolint:gosec
}
c.httpclient = &http.Client{Transport: tr}
if !c.DisableServer {

View File

@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2017, Baozisoftware
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,39 @@
# qrcode-terminal-go
QRCode terminal for golang.
# Example
```go
package main
import "github.com/Baozisoftware/qrcode-terminal-go"
func main() {
Test1()
Test2()
}
func Test1(){
content := "Hello, 世界"
obj := qrcodeTerminal.New()
obj.Get(content).Print()
}
func Test2(){
content := "https://github.com/Baozisoftware/qrcode-terminal-go"
obj := qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightBlue,qrcodeTerminal.ConsoleColors.BrightGreen,qrcodeTerminal.QRCodeRecoveryLevels.Low)
obj.Get([]byte(content)).Print()
}
```
## Screenshots
### Windows XP
![winxp](https://github.com/Baozisoftware/qrcode-terminal-go/blob/master/screenshots/winxp.png)
### Windows 7
![win7](https://github.com/Baozisoftware/qrcode-terminal-go/blob/master/screenshots/win7.png)
### Windows 10
![win10](https://github.com/Baozisoftware/qrcode-terminal-go/blob/master/screenshots/win10.png)
### Ubuntu
![ubuntu](https://github.com/Baozisoftware/qrcode-terminal-go/blob/master/screenshots/ubuntu.png)
### macOS
![macos](https://github.com/Baozisoftware/qrcode-terminal-go/blob/master/screenshots/macos.png)

View File

@ -0,0 +1,155 @@
package qrcodeTerminal
import (
"fmt"
"github.com/skip2/go-qrcode"
"github.com/mattn/go-colorable"
"image/png"
nbytes "bytes"
)
type consoleColor string
type consoleColors struct {
NormalBlack consoleColor
NormalRed consoleColor
NormalGreen consoleColor
NormalYellow consoleColor
NormalBlue consoleColor
NormalMagenta consoleColor
NormalCyan consoleColor
NormalWhite consoleColor
BrightBlack consoleColor
BrightRed consoleColor
BrightGreen consoleColor
BrightYellow consoleColor
BrightBlue consoleColor
BrightMagenta consoleColor
BrightCyan consoleColor
BrightWhite consoleColor
}
type qrcodeRecoveryLevel qrcode.RecoveryLevel
type qrcodeRecoveryLevels struct {
Low qrcodeRecoveryLevel
Medium qrcodeRecoveryLevel
High qrcodeRecoveryLevel
Highest qrcodeRecoveryLevel
}
var (
ConsoleColors consoleColors = consoleColors{
NormalBlack: "\033[38;5;0m \033[0m",
NormalRed: "\033[38;5;1m \033[0m",
NormalGreen: "\033[38;5;2m \033[0m",
NormalYellow: "\033[38;5;3m \033[0m",
NormalBlue: "\033[38;5;4m \033[0m",
NormalMagenta: "\033[38;5;5m \033[0m",
NormalCyan: "\033[38;5;6m \033[0m",
NormalWhite: "\033[38;5;7m \033[0m",
BrightBlack: "\033[48;5;0m \033[0m",
BrightRed: "\033[48;5;1m \033[0m",
BrightGreen: "\033[48;5;2m \033[0m",
BrightYellow: "\033[48;5;3m \033[0m",
BrightBlue: "\033[48;5;4m \033[0m",
BrightMagenta: "\033[48;5;5m \033[0m",
BrightCyan: "\033[48;5;6m \033[0m",
BrightWhite: "\033[48;5;7m \033[0m"}
QRCodeRecoveryLevels = qrcodeRecoveryLevels{
Low: qrcodeRecoveryLevel(qrcode.Low),
Medium: qrcodeRecoveryLevel(qrcode.Medium),
High: qrcodeRecoveryLevel(qrcode.High),
Highest: qrcodeRecoveryLevel(qrcode.Highest)}
)
type QRCodeString string
func (v *QRCodeString) Print() {
fmt.Fprint(outer, *v)
}
type qrcodeTerminal struct {
front consoleColor
back consoleColor
level qrcodeRecoveryLevel
}
func (v *qrcodeTerminal) Get(content interface{}) (result *QRCodeString) {
var qr *qrcode.QRCode
var err error
if t, ok := content.(string); ok {
qr, err = qrcode.New(t, qrcode.RecoveryLevel(v.level))
} else if t, ok := content.([]byte); ok {
qr, err = qrcode.New(string(t), qrcode.RecoveryLevel(v.level))
}
if qr != nil && err == nil {
data := qr.Bitmap()
result = v.getQRCodeString(data)
}
return
}
func (v *qrcodeTerminal) Get2(bytes []byte) (result *QRCodeString) {
data, err := parseQR(bytes)
if err == nil {
result = v.getQRCodeString(data)
}
return
}
func New2(front, back consoleColor, level qrcodeRecoveryLevel) *qrcodeTerminal {
obj := qrcodeTerminal{front: front, back: back, level: level}
return &obj
}
func New() *qrcodeTerminal {
front, back, level := ConsoleColors.BrightBlack, ConsoleColors.BrightWhite, QRCodeRecoveryLevels.Medium
return New2(front, back, level)
}
func (v *qrcodeTerminal) getQRCodeString(data [][]bool) (result *QRCodeString) {
str := ""
for ir, row := range data {
lr := len(row)
if ir == 0 || ir == 1 || ir == 2 ||
ir == lr-1 || ir == lr-2 || ir == lr-3 {
continue
}
for ic, col := range row {
lc := len(data)
if ic == 0 || ic == 1 || ic == 2 ||
ic == lc-1 || ic == lc-2 || ic == lc-3 {
continue
}
if col {
str += fmt.Sprint(v.front)
} else {
str += fmt.Sprint(v.back)
}
}
str += fmt.Sprintln()
}
obj := QRCodeString(str)
result = &obj
return
}
func parseQR(bytes []byte) (data [][]bool, err error) {
r := nbytes.NewReader(bytes)
img, err := png.Decode(r)
if err == nil {
rect := img.Bounds()
mx, my := rect.Max.X, rect.Max.Y
data = make([][]bool, mx)
for x := 0; x < mx; x++ {
data[x] = make([]bool, my)
for y := 0; y < my; y++ {
c := img.At(x, y)
r, _, _, _ := c.RGBA()
data[x][y] = r == 0
}
}
}
return
}
var outer = colorable.NewColorableStdout()

19
vendor/github.com/Jeffail/gabs/LICENSE generated vendored Normal file
View File

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

315
vendor/github.com/Jeffail/gabs/README.md generated vendored Normal file
View File

@ -0,0 +1,315 @@
![Gabs](gabs_logo.png "Gabs")
Gabs is a small utility for dealing with dynamic or unknown JSON structures in
golang. It's pretty much just a helpful wrapper around the golang
`json.Marshal/json.Unmarshal` behaviour and `map[string]interface{}` objects.
It does nothing spectacular except for being fabulous.
https://godoc.org/github.com/Jeffail/gabs
## How to install:
``` bash
go get github.com/Jeffail/gabs
```
## How to use
### Parsing and searching JSON
``` go
...
import "github.com/Jeffail/gabs"
jsonParsed, err := gabs.ParseJSON([]byte(`{
"outter":{
"inner":{
"value1":10,
"value2":22
},
"alsoInner":{
"value1":20
}
}
}`))
var value float64
var ok bool
value, ok = jsonParsed.Path("outter.inner.value1").Data().(float64)
// value == 10.0, ok == true
value, ok = jsonParsed.Search("outter", "inner", "value1").Data().(float64)
// value == 10.0, ok == true
value, ok = jsonParsed.Path("does.not.exist").Data().(float64)
// value == 0.0, ok == false
exists := jsonParsed.Exists("outter", "inner", "value1")
// exists == true
exists := jsonParsed.Exists("does", "not", "exist")
// exists == false
exists := jsonParsed.ExistsP("does.not.exist")
// exists == false
...
```
### Iterating objects
``` go
...
jsonParsed, _ := gabs.ParseJSON([]byte(`{"object":{ "first": 1, "second": 2, "third": 3 }}`))
// S is shorthand for Search
children, _ := jsonParsed.S("object").ChildrenMap()
for key, child := range children {
fmt.Printf("key: %v, value: %v\n", key, child.Data().(string))
}
...
```
### Iterating arrays
``` go
...
jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ "first", "second", "third" ]}`))
// S is shorthand for Search
children, _ := jsonParsed.S("array").Children()
for _, child := range children {
fmt.Println(child.Data().(string))
}
...
```
Will print:
```
first
second
third
```
Children() will return all children of an array in order. This also works on
objects, however, the children will be returned in a random order.
### Searching through arrays
If your JSON structure contains arrays you can still search the fields of the
objects within the array, this returns a JSON array containing the results for
each element.
``` go
...
jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ {"value":1}, {"value":2}, {"value":3} ]}`))
fmt.Println(jsonParsed.Path("array.value").String())
...
```
Will print:
```
[1,2,3]
```
### Generating JSON
``` go
...
jsonObj := gabs.New()
// or gabs.Consume(jsonObject) to work on an existing map[string]interface{}
jsonObj.Set(10, "outter", "inner", "value")
jsonObj.SetP(20, "outter.inner.value2")
jsonObj.Set(30, "outter", "inner2", "value3")
fmt.Println(jsonObj.String())
...
```
Will print:
```
{"outter":{"inner":{"value":10,"value2":20},"inner2":{"value3":30}}}
```
To pretty-print:
``` go
...
fmt.Println(jsonObj.StringIndent("", " "))
...
```
Will print:
```
{
"outter": {
"inner": {
"value": 10,
"value2": 20
},
"inner2": {
"value3": 30
}
}
}
```
### Generating Arrays
``` go
...
jsonObj := gabs.New()
jsonObj.Array("foo", "array")
// Or .ArrayP("foo.array")
jsonObj.ArrayAppend(10, "foo", "array")
jsonObj.ArrayAppend(20, "foo", "array")
jsonObj.ArrayAppend(30, "foo", "array")
fmt.Println(jsonObj.String())
...
```
Will print:
```
{"foo":{"array":[10,20,30]}}
```
Working with arrays by index:
``` go
...
jsonObj := gabs.New()
// Create an array with the length of 3
jsonObj.ArrayOfSize(3, "foo")
jsonObj.S("foo").SetIndex("test1", 0)
jsonObj.S("foo").SetIndex("test2", 1)
// Create an embedded array with the length of 3
jsonObj.S("foo").ArrayOfSizeI(3, 2)
jsonObj.S("foo").Index(2).SetIndex(1, 0)
jsonObj.S("foo").Index(2).SetIndex(2, 1)
jsonObj.S("foo").Index(2).SetIndex(3, 2)
fmt.Println(jsonObj.String())
...
```
Will print:
```
{"foo":["test1","test2",[1,2,3]]}
```
### Converting back to JSON
This is the easiest part:
``` go
...
jsonParsedObj, _ := gabs.ParseJSON([]byte(`{
"outter":{
"values":{
"first":10,
"second":11
}
},
"outter2":"hello world"
}`))
jsonOutput := jsonParsedObj.String()
// Becomes `{"outter":{"values":{"first":10,"second":11}},"outter2":"hello world"}`
...
```
And to serialize a specific segment is as simple as:
``` go
...
jsonParsedObj := gabs.ParseJSON([]byte(`{
"outter":{
"values":{
"first":10,
"second":11
}
},
"outter2":"hello world"
}`))
jsonOutput := jsonParsedObj.Search("outter").String()
// Becomes `{"values":{"first":10,"second":11}}`
...
```
### Merge two containers
You can merge a JSON structure into an existing one, where collisions will be
converted into a JSON array.
``` go
jsonParsed1, _ := ParseJSON([]byte(`{"outter": {"value1": "one"}}`))
jsonParsed2, _ := ParseJSON([]byte(`{"outter": {"inner": {"value3": "three"}}, "outter2": {"value2": "two"}}`))
jsonParsed1.Merge(jsonParsed2)
// Becomes `{"outter":{"inner":{"value3":"three"},"value1":"one"},"outter2":{"value2":"two"}}`
```
Arrays are merged:
``` go
jsonParsed1, _ := ParseJSON([]byte(`{"array": ["one"]}`))
jsonParsed2, _ := ParseJSON([]byte(`{"array": ["two"]}`))
jsonParsed1.Merge(jsonParsed2)
// Becomes `{"array":["one", "two"]}`
```
### Parsing Numbers
Gabs uses the `json` package under the bonnet, which by default will parse all
number values into `float64`. If you need to parse `Int` values then you should
use a `json.Decoder` (https://golang.org/pkg/encoding/json/#Decoder):
``` go
sample := []byte(`{"test":{"int":10, "float":6.66}}`)
dec := json.NewDecoder(bytes.NewReader(sample))
dec.UseNumber()
val, err := gabs.ParseJSONDecoder(dec)
if err != nil {
t.Errorf("Failed to parse: %v", err)
return
}
intValue, err := val.Path("test.int").Data().(json.Number).Int64()
```

581
vendor/github.com/Jeffail/gabs/gabs.go generated vendored Normal file
View File

@ -0,0 +1,581 @@
/*
Copyright (c) 2014 Ashley Jeffs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// Package gabs implements a simplified wrapper around creating and parsing JSON.
package gabs
import (
"bytes"
"encoding/json"
"errors"
"io"
"io/ioutil"
"strings"
)
//--------------------------------------------------------------------------------------------------
var (
// ErrOutOfBounds - Index out of bounds.
ErrOutOfBounds = errors.New("out of bounds")
// ErrNotObjOrArray - The target is not an object or array type.
ErrNotObjOrArray = errors.New("not an object or array")
// ErrNotObj - The target is not an object type.
ErrNotObj = errors.New("not an object")
// ErrNotArray - The target is not an array type.
ErrNotArray = errors.New("not an array")
// ErrPathCollision - Creating a path failed because an element collided with an existing value.
ErrPathCollision = errors.New("encountered value collision whilst building path")
// ErrInvalidInputObj - The input value was not a map[string]interface{}.
ErrInvalidInputObj = errors.New("invalid input object")
// ErrInvalidInputText - The input data could not be parsed.
ErrInvalidInputText = errors.New("input text could not be parsed")
// ErrInvalidPath - The filepath was not valid.
ErrInvalidPath = errors.New("invalid file path")
// ErrInvalidBuffer - The input buffer contained an invalid JSON string
ErrInvalidBuffer = errors.New("input buffer contained invalid JSON")
)
//--------------------------------------------------------------------------------------------------
// Container - an internal structure that holds a reference to the core interface map of the parsed
// json. Use this container to move context.
type Container struct {
object interface{}
}
// Data - Return the contained data as an interface{}.
func (g *Container) Data() interface{} {
if g == nil {
return nil
}
return g.object
}
//--------------------------------------------------------------------------------------------------
// Path - Search for a value using dot notation.
func (g *Container) Path(path string) *Container {
return g.Search(strings.Split(path, ".")...)
}
// Search - Attempt to find and return an object within the JSON structure by specifying the
// hierarchy of field names to locate the target. If the search encounters an array and has not
// reached the end target then it will iterate each object of the array for the target and return
// all of the results in a JSON array.
func (g *Container) Search(hierarchy ...string) *Container {
var object interface{}
object = g.Data()
for target := 0; target < len(hierarchy); target++ {
if mmap, ok := object.(map[string]interface{}); ok {
object, ok = mmap[hierarchy[target]]
if !ok {
return nil
}
} else if marray, ok := object.([]interface{}); ok {
tmpArray := []interface{}{}
for _, val := range marray {
tmpGabs := &Container{val}
res := tmpGabs.Search(hierarchy[target:]...)
if res != nil {
tmpArray = append(tmpArray, res.Data())
}
}
if len(tmpArray) == 0 {
return nil
}
return &Container{tmpArray}
} else {
return nil
}
}
return &Container{object}
}
// S - Shorthand method, does the same thing as Search.
func (g *Container) S(hierarchy ...string) *Container {
return g.Search(hierarchy...)
}
// Exists - Checks whether a path exists.
func (g *Container) Exists(hierarchy ...string) bool {
return g.Search(hierarchy...) != nil
}
// ExistsP - Checks whether a dot notation path exists.
func (g *Container) ExistsP(path string) bool {
return g.Exists(strings.Split(path, ".")...)
}
// Index - Attempt to find and return an object within a JSON array by index.
func (g *Container) Index(index int) *Container {
if array, ok := g.Data().([]interface{}); ok {
if index >= len(array) {
return &Container{nil}
}
return &Container{array[index]}
}
return &Container{nil}
}
// Children - Return a slice of all the children of the array. This also works for objects, however,
// the children returned for an object will NOT be in order and you lose the names of the returned
// objects this way.
func (g *Container) Children() ([]*Container, error) {
if array, ok := g.Data().([]interface{}); ok {
children := make([]*Container, len(array))
for i := 0; i < len(array); i++ {
children[i] = &Container{array[i]}
}
return children, nil
}
if mmap, ok := g.Data().(map[string]interface{}); ok {
children := []*Container{}
for _, obj := range mmap {
children = append(children, &Container{obj})
}
return children, nil
}
return nil, ErrNotObjOrArray
}
// ChildrenMap - Return a map of all the children of an object.
func (g *Container) ChildrenMap() (map[string]*Container, error) {
if mmap, ok := g.Data().(map[string]interface{}); ok {
children := map[string]*Container{}
for name, obj := range mmap {
children[name] = &Container{obj}
}
return children, nil
}
return nil, ErrNotObj
}
//--------------------------------------------------------------------------------------------------
// Set - Set the value of a field at a JSON path, any parts of the path that do not exist will be
// constructed, and if a collision occurs with a non object type whilst iterating the path an error
// is returned.
func (g *Container) Set(value interface{}, path ...string) (*Container, error) {
if len(path) == 0 {
g.object = value
return g, nil
}
var object interface{}
if g.object == nil {
g.object = map[string]interface{}{}
}
object = g.object
for target := 0; target < len(path); target++ {
if mmap, ok := object.(map[string]interface{}); ok {
if target == len(path)-1 {
mmap[path[target]] = value
} else if mmap[path[target]] == nil {
mmap[path[target]] = map[string]interface{}{}
}
object = mmap[path[target]]
} else {
return &Container{nil}, ErrPathCollision
}
}
return &Container{object}, nil
}
// SetP - Does the same as Set, but using a dot notation JSON path.
func (g *Container) SetP(value interface{}, path string) (*Container, error) {
return g.Set(value, strings.Split(path, ".")...)
}
// SetIndex - Set a value of an array element based on the index.
func (g *Container) SetIndex(value interface{}, index int) (*Container, error) {
if array, ok := g.Data().([]interface{}); ok {
if index >= len(array) {
return &Container{nil}, ErrOutOfBounds
}
array[index] = value
return &Container{array[index]}, nil
}
return &Container{nil}, ErrNotArray
}
// Object - Create a new JSON object at a path. Returns an error if the path contains a collision
// with a non object type.
func (g *Container) Object(path ...string) (*Container, error) {
return g.Set(map[string]interface{}{}, path...)
}
// ObjectP - Does the same as Object, but using a dot notation JSON path.
func (g *Container) ObjectP(path string) (*Container, error) {
return g.Object(strings.Split(path, ".")...)
}
// ObjectI - Create a new JSON object at an array index. Returns an error if the object is not an
// array or the index is out of bounds.
func (g *Container) ObjectI(index int) (*Container, error) {
return g.SetIndex(map[string]interface{}{}, index)
}
// Array - Create a new JSON array at a path. Returns an error if the path contains a collision with
// a non object type.
func (g *Container) Array(path ...string) (*Container, error) {
return g.Set([]interface{}{}, path...)
}
// ArrayP - Does the same as Array, but using a dot notation JSON path.
func (g *Container) ArrayP(path string) (*Container, error) {
return g.Array(strings.Split(path, ".")...)
}
// ArrayI - Create a new JSON array at an array index. Returns an error if the object is not an
// array or the index is out of bounds.
func (g *Container) ArrayI(index int) (*Container, error) {
return g.SetIndex([]interface{}{}, index)
}
// ArrayOfSize - Create a new JSON array of a particular size at a path. Returns an error if the
// path contains a collision with a non object type.
func (g *Container) ArrayOfSize(size int, path ...string) (*Container, error) {
a := make([]interface{}, size)
return g.Set(a, path...)
}
// ArrayOfSizeP - Does the same as ArrayOfSize, but using a dot notation JSON path.
func (g *Container) ArrayOfSizeP(size int, path string) (*Container, error) {
return g.ArrayOfSize(size, strings.Split(path, ".")...)
}
// ArrayOfSizeI - Create a new JSON array of a particular size at an array index. Returns an error
// if the object is not an array or the index is out of bounds.
func (g *Container) ArrayOfSizeI(size, index int) (*Container, error) {
a := make([]interface{}, size)
return g.SetIndex(a, index)
}
// Delete - Delete an element at a JSON path, an error is returned if the element does not exist.
func (g *Container) Delete(path ...string) error {
var object interface{}
if g.object == nil {
return ErrNotObj
}
object = g.object
for target := 0; target < len(path); target++ {
if mmap, ok := object.(map[string]interface{}); ok {
if target == len(path)-1 {
if _, ok := mmap[path[target]]; ok {
delete(mmap, path[target])
} else {
return ErrNotObj
}
}
object = mmap[path[target]]
} else {
return ErrNotObj
}
}
return nil
}
// DeleteP - Does the same as Delete, but using a dot notation JSON path.
func (g *Container) DeleteP(path string) error {
return g.Delete(strings.Split(path, ".")...)
}
// Merge - Merges two gabs-containers
func (g *Container) Merge(toMerge *Container) error {
var recursiveFnc func(map[string]interface{}, []string) error
recursiveFnc = func(mmap map[string]interface{}, path []string) error {
for key, value := range mmap {
newPath := append(path, key)
if g.Exists(newPath...) {
target := g.Search(newPath...)
switch t := value.(type) {
case map[string]interface{}:
switch targetV := target.Data().(type) {
case map[string]interface{}:
if err := recursiveFnc(t, newPath); err != nil {
return err
}
case []interface{}:
g.Set(append(targetV, t), newPath...)
default:
newSlice := append([]interface{}{}, targetV)
g.Set(append(newSlice, t), newPath...)
}
case []interface{}:
for _, valueOfSlice := range t {
if err := g.ArrayAppend(valueOfSlice, newPath...); err != nil {
return err
}
}
default:
switch targetV := target.Data().(type) {
case []interface{}:
g.Set(append(targetV, t), newPath...)
default:
newSlice := append([]interface{}{}, targetV)
g.Set(append(newSlice, t), newPath...)
}
}
} else {
// path doesn't exist. So set the value
if _, err := g.Set(value, newPath...); err != nil {
return err
}
}
}
return nil
}
if mmap, ok := toMerge.Data().(map[string]interface{}); ok {
return recursiveFnc(mmap, []string{})
}
return nil
}
//--------------------------------------------------------------------------------------------------
/*
Array modification/search - Keeping these options simple right now, no need for anything more
complicated since you can just cast to []interface{}, modify and then reassign with Set.
*/
// ArrayAppend - Append a value onto a JSON array. If the target is not a JSON array then it will be
// converted into one, with its contents as the first element of the array.
func (g *Container) ArrayAppend(value interface{}, path ...string) error {
if array, ok := g.Search(path...).Data().([]interface{}); ok {
array = append(array, value)
_, err := g.Set(array, path...)
return err
}
newArray := []interface{}{}
if d := g.Search(path...).Data(); d != nil {
newArray = append(newArray, d)
}
newArray = append(newArray, value)
_, err := g.Set(newArray, path...)
return err
}
// ArrayAppendP - Append a value onto a JSON array using a dot notation JSON path.
func (g *Container) ArrayAppendP(value interface{}, path string) error {
return g.ArrayAppend(value, strings.Split(path, ".")...)
}
// ArrayRemove - Remove an element from a JSON array.
func (g *Container) ArrayRemove(index int, path ...string) error {
if index < 0 {
return ErrOutOfBounds
}
array, ok := g.Search(path...).Data().([]interface{})
if !ok {
return ErrNotArray
}
if index < len(array) {
array = append(array[:index], array[index+1:]...)
} else {
return ErrOutOfBounds
}
_, err := g.Set(array, path...)
return err
}
// ArrayRemoveP - Remove an element from a JSON array using a dot notation JSON path.
func (g *Container) ArrayRemoveP(index int, path string) error {
return g.ArrayRemove(index, strings.Split(path, ".")...)
}
// ArrayElement - Access an element from a JSON array.
func (g *Container) ArrayElement(index int, path ...string) (*Container, error) {
if index < 0 {
return &Container{nil}, ErrOutOfBounds
}
array, ok := g.Search(path...).Data().([]interface{})
if !ok {
return &Container{nil}, ErrNotArray
}
if index < len(array) {
return &Container{array[index]}, nil
}
return &Container{nil}, ErrOutOfBounds
}
// ArrayElementP - Access an element from a JSON array using a dot notation JSON path.
func (g *Container) ArrayElementP(index int, path string) (*Container, error) {
return g.ArrayElement(index, strings.Split(path, ".")...)
}
// ArrayCount - Count the number of elements in a JSON array.
func (g *Container) ArrayCount(path ...string) (int, error) {
if array, ok := g.Search(path...).Data().([]interface{}); ok {
return len(array), nil
}
return 0, ErrNotArray
}
// ArrayCountP - Count the number of elements in a JSON array using a dot notation JSON path.
func (g *Container) ArrayCountP(path string) (int, error) {
return g.ArrayCount(strings.Split(path, ".")...)
}
//--------------------------------------------------------------------------------------------------
// Bytes - Converts the contained object back to a JSON []byte blob.
func (g *Container) Bytes() []byte {
if g.Data() != nil {
if bytes, err := json.Marshal(g.object); err == nil {
return bytes
}
}
return []byte("{}")
}
// BytesIndent - Converts the contained object to a JSON []byte blob formatted with prefix, indent.
func (g *Container) BytesIndent(prefix string, indent string) []byte {
if g.object != nil {
if bytes, err := json.MarshalIndent(g.object, prefix, indent); err == nil {
return bytes
}
}
return []byte("{}")
}
// String - Converts the contained object to a JSON formatted string.
func (g *Container) String() string {
return string(g.Bytes())
}
// StringIndent - Converts the contained object back to a JSON formatted string with prefix, indent.
func (g *Container) StringIndent(prefix string, indent string) string {
return string(g.BytesIndent(prefix, indent))
}
// EncodeOpt is a functional option for the EncodeJSON method.
type EncodeOpt func(e *json.Encoder)
// EncodeOptHTMLEscape sets the encoder to escape the JSON for html.
func EncodeOptHTMLEscape(doEscape bool) EncodeOpt {
return func(e *json.Encoder) {
e.SetEscapeHTML(doEscape)
}
}
// EncodeOptIndent sets the encoder to indent the JSON output.
func EncodeOptIndent(prefix string, indent string) EncodeOpt {
return func(e *json.Encoder) {
e.SetIndent(prefix, indent)
}
}
// EncodeJSON - Encodes the contained object back to a JSON formatted []byte
// using a variant list of modifier functions for the encoder being used.
// Functions for modifying the output are prefixed with EncodeOpt, e.g.
// EncodeOptHTMLEscape.
func (g *Container) EncodeJSON(encodeOpts ...EncodeOpt) []byte {
var b bytes.Buffer
encoder := json.NewEncoder(&b)
encoder.SetEscapeHTML(false) // Do not escape by default.
for _, opt := range encodeOpts {
opt(encoder)
}
if err := encoder.Encode(g.object); err != nil {
return []byte("{}")
}
result := b.Bytes()
if len(result) > 0 {
result = result[:len(result)-1]
}
return result
}
// New - Create a new gabs JSON object.
func New() *Container {
return &Container{map[string]interface{}{}}
}
// Consume - Gobble up an already converted JSON object, or a fresh map[string]interface{} object.
func Consume(root interface{}) (*Container, error) {
return &Container{root}, nil
}
// ParseJSON - Convert a string into a representation of the parsed JSON.
func ParseJSON(sample []byte) (*Container, error) {
var gabs Container
if err := json.Unmarshal(sample, &gabs.object); err != nil {
return nil, err
}
return &gabs, nil
}
// ParseJSONDecoder - Convert a json.Decoder into a representation of the parsed JSON.
func ParseJSONDecoder(decoder *json.Decoder) (*Container, error) {
var gabs Container
if err := decoder.Decode(&gabs.object); err != nil {
return nil, err
}
return &gabs, nil
}
// ParseJSONFile - Read a file and convert into a representation of the parsed JSON.
func ParseJSONFile(path string) (*Container, error) {
if len(path) > 0 {
cBytes, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
container, err := ParseJSON(cBytes)
if err != nil {
return nil, err
}
return container, nil
}
return nil, ErrInvalidPath
}
// ParseJSONBuffer - Read the contents of a buffer into a representation of the parsed JSON.
func ParseJSONBuffer(buffer io.Reader) (*Container, error) {
var gabs Container
jsonDecoder := json.NewDecoder(buffer)
if err := jsonDecoder.Decode(&gabs.object); err != nil {
return nil, err
}
return &gabs, nil
}
//--------------------------------------------------------------------------------------------------

BIN
vendor/github.com/Jeffail/gabs/gabs_logo.png generated vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View File

@ -2,13 +2,14 @@ package steam
import (
"crypto/sha1"
"sync/atomic"
"time"
. "github.com/Philipp15b/go-steam/protocol"
. "github.com/Philipp15b/go-steam/protocol/protobuf"
. "github.com/Philipp15b/go-steam/protocol/steamlang"
. "github.com/Philipp15b/go-steam/steamid"
"github.com/golang/protobuf/proto"
"sync/atomic"
"time"
)
type Auth struct {
@ -19,23 +20,41 @@ type Auth struct {
type SentryHash []byte
type LogOnDetails struct {
Username string
Password string
AuthCode string
Username string
// If logging into an account without a login key, the account's password.
Password string
// If you have a Steam Guard email code, you can provide it here.
AuthCode string
// If you have a Steam Guard mobile two-factor authentication code, you can provide it here.
TwoFactorCode string
SentryFileHash SentryHash
LoginKey string
// true if you want to get a login key which can be used in lieu of
// a password for subsequent logins. false or omitted otherwise.
ShouldRememberPassword bool
}
// Log on with the given details. You must always specify username and
// password. For the first login, don't set an authcode or a hash and you'll receive an error
// password OR username and loginkey. For the first login, don't set an authcode or a hash and you'll
// receive an error (EResult_AccountLogonDenied)
// and Steam will send you an authcode. Then you have to login again, this time with the authcode.
// Shortly after logging in, you'll receive a MachineAuthUpdateEvent with a hash which allows
// you to login without using an authcode in the future.
//
// If you don't use Steam Guard, username and password are enough.
//
// After the event EMsg_ClientNewLoginKey is received you can use the LoginKey
// to login instead of using the password.
func (a *Auth) LogOn(details *LogOnDetails) {
if len(details.Username) == 0 || len(details.Password) == 0 {
panic("Username and password must be set!")
if details.Username == "" {
panic("Username must be set!")
}
if details.Password == "" && details.LoginKey == "" {
panic("Password or LoginKey must be set!")
}
logon := new(CMsgClientLogon)
@ -50,6 +69,12 @@ func (a *Auth) LogOn(details *LogOnDetails) {
logon.ClientLanguage = proto.String("english")
logon.ProtocolVersion = proto.Uint32(MsgClientLogon_CurrentProtocol)
logon.ShaSentryfile = details.SentryFileHash
if details.LoginKey != "" {
logon.LoginKey = proto.String(details.LoginKey)
}
if details.ShouldRememberPassword {
logon.ShouldRememberPassword = proto.Bool(details.ShouldRememberPassword)
}
atomic.StoreUint64(&a.client.steamId, uint64(NewIdAdv(0, 1, int32(EUniverse_Public), int32(EAccountType_Individual))))

View File

@ -1,2 +0,0 @@
*.sw[op]
.DS_Store

View File

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

View File

@ -1,14 +0,0 @@
# This is an unmaintained fork, left only so it doesn't break imports.
Please see http://log4go.googlecode.com/
Installation:
- Run `goinstall log4go.googlecode.com/hg`
Usage:
- Add the following import:
import l4g "log4go.googlecode.com/hg"
Acknowledgements:
- pomack
For providing awesome patches to bring log4go up to the latest Go spec

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
vendor/github.com/d5/tengo/.gitignore generated vendored Normal file
View File

@ -0,0 +1 @@
dist/

23
vendor/github.com/d5/tengo/.goreleaser.yml generated vendored Normal file
View File

@ -0,0 +1,23 @@
builds:
- env:
- CGO_ENABLED=0
main: ./cmd/tengo/main.go
goos:
- darwin
- linux
- windows
- env:
- CGO_ENABLED=0
main: ./cmd/tengomin/main.go
binary: tengomin
goos:
- darwin
- linux
- windows
archive:
files:
- none*
checksum:
name_template: 'checksums.txt'
changelog:
sort: asc

17
vendor/github.com/d5/tengo/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,17 @@
language: go
go:
- 1.9
install:
- go get -u golang.org/x/lint/golint
script:
- make test
deploy:
- provider: script
skip_cleanup: true
script: curl -sL https://git.io/goreleaser | bash
on:
tags: true

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2015 Dmitri Shuralyov
Copyright (c) 2019 Daniel Kang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

11
vendor/github.com/d5/tengo/Makefile generated vendored Normal file
View File

@ -0,0 +1,11 @@
vet:
go vet ./...
lint:
golint -set_exit_status ./...
test: vet lint
go test -race -cover ./...
fmt:
go fmt ./...

74
vendor/github.com/d5/tengo/README.md generated vendored Normal file
View File

@ -0,0 +1,74 @@
<p align="center">
<img src="https://raw.githubusercontent.com/d5/tengolang.com/master/logo_400.png" width="200" height="200">
</p>
# The Tengo Language
[![GoDoc](https://godoc.org/github.com/d5/tengo?status.svg)](https://godoc.org/github.com/d5/tengo/script)
[![Go Report Card](https://goreportcard.com/badge/github.com/d5/tengo)](https://goreportcard.com/report/github.com/d5/tengo)
[![Build Status](https://travis-ci.org/d5/tengo.svg?branch=master)](https://travis-ci.org/d5/tengo)
[![](https://img.shields.io/badge/Support%20Tengo-%241-brightgreen.svg)](https://www.patreon.com/tengolang)
**Tengo is a small, dynamic, fast, secure script language for Go.**
Tengo is **[fast](#benchmark)** and secure because it's compiled/executed as bytecode on stack-based VM that's written in native Go.
```golang
/* The Tengo Language */
each := func(seq, fn) {
for x in seq { fn(x) }
}
sum := func(init, seq) {
each(seq, func(x) { init += x })
return init
}
n := sum(0, [1, 2, 3]) // == 6
s := sum("", [1, 2, 3]) // == "123"
```
> Run this code in the [Playground](https://tengolang.com/?s=d01cf9ed81daba939e26618530eb171f7397d9c9)
## Features
- Simple and highly readable [Syntax](https://github.com/d5/tengo/blob/master/docs/tutorial.md)
- Dynamic typing with type coercion
- Higher-order functions and closures
- Immutable values
- Garbage collection
- [Securely Embeddable](https://github.com/d5/tengo/blob/master/docs/interoperability.md) and [Extensible](https://github.com/d5/tengo/blob/master/docs/objects.md)
- Compiler/runtime written in native Go _(no external deps or cgo)_
- Executable as a [standalone](https://github.com/d5/tengo/blob/master/docs/tengo-cli.md) language / REPL
## Benchmark
| | fib(35) | fibt(35) | Type |
| :--- | ---: | ---: | :---: |
| Go | `58ms` | `4ms` | Go (native) |
| [**Tengo**](https://github.com/d5/tengo) | `4,180ms` | `5ms` | VM on Go |
| Lua | `1,695ms` | `3ms` | Lua (native) |
| [go-lua](https://github.com/Shopify/go-lua) | `5,163ms` | `5ms` | Lua VM on Go |
| [GopherLua](https://github.com/yuin/gopher-lua) | `5,525ms` | `5ms` | Lua VM on Go |
| Python | `3,097ms` | `27ms` | Python (native) |
| [starlark-go](https://github.com/google/starlark-go) | `15,307ms` | `5ms` | Python-like Interpreter on Go |
| [gpython](https://github.com/go-python/gpython) | `17,656ms` | `5ms` | Python Interpreter on Go |
| [goja](https://github.com/dop251/goja) | `6,876ms` | `5ms` | JS VM on Go |
| [otto](https://github.com/robertkrimen/otto) | `81,886ms` | `12ms` | JS Interpreter on Go |
| [Anko](https://github.com/mattn/anko) | `97,517ms` | `14ms` | Interpreter on Go |
_* [fib(35)](https://github.com/d5/tengobench/blob/master/code/fib.tengo): Fibonacci(35)_
_* [fibt(35)](https://github.com/d5/tengobench/blob/master/code/fibtc.tengo): [tail-call](https://en.wikipedia.org/wiki/Tail_call) version of Fibonacci(35)_
_* **Go** does not read the source code from file, while all other cases do_
_* See [here](https://github.com/d5/tengobench) for commands/codes used_
## References
- [Language Syntax](https://github.com/d5/tengo/blob/master/docs/tutorial.md)
- [Object Types](https://github.com/d5/tengo/blob/master/docs/objects.md)
- [Runtime Types](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) and [Operators](https://github.com/d5/tengo/blob/master/docs/operators.md)
- [Builtin Functions](https://github.com/d5/tengo/blob/master/docs/builtins.md)
- [Interoperability](https://github.com/d5/tengo/blob/master/docs/interoperability.md)
- [Tengo CLI](https://github.com/d5/tengo/blob/master/docs/tengo-cli.md)
- [Standard Library](https://github.com/d5/tengo/blob/master/docs/stdlib.md)

35
vendor/github.com/d5/tengo/compiler/ast/array_lit.go generated vendored Normal file
View File

@ -0,0 +1,35 @@
package ast
import (
"strings"
"github.com/d5/tengo/compiler/source"
)
// ArrayLit represents an array literal.
type ArrayLit struct {
Elements []Expr
LBrack source.Pos
RBrack source.Pos
}
func (e *ArrayLit) exprNode() {}
// Pos returns the position of first character belonging to the node.
func (e *ArrayLit) Pos() source.Pos {
return e.LBrack
}
// End returns the position of first character immediately after the node.
func (e *ArrayLit) End() source.Pos {
return e.RBrack + 1
}
func (e *ArrayLit) String() string {
var elements []string
for _, m := range e.Elements {
elements = append(elements, m.String())
}
return "[" + strings.Join(elements, ", ") + "]"
}

40
vendor/github.com/d5/tengo/compiler/ast/assign_stmt.go generated vendored Normal file
View File

@ -0,0 +1,40 @@
package ast
import (
"strings"
"github.com/d5/tengo/compiler/source"
"github.com/d5/tengo/compiler/token"
)
// AssignStmt represents an assignment statement.
type AssignStmt struct {
LHS []Expr
RHS []Expr
Token token.Token
TokenPos source.Pos
}
func (s *AssignStmt) stmtNode() {}
// Pos returns the position of first character belonging to the node.
func (s *AssignStmt) Pos() source.Pos {
return s.LHS[0].Pos()
}
// End returns the position of first character immediately after the node.
func (s *AssignStmt) End() source.Pos {
return s.RHS[len(s.RHS)-1].End()
}
func (s *AssignStmt) String() string {
var lhs, rhs []string
for _, e := range s.LHS {
lhs = append(lhs, e.String())
}
for _, e := range s.RHS {
rhs = append(rhs, e.String())
}
return strings.Join(lhs, ", ") + " " + s.Token.String() + " " + strings.Join(rhs, ", ")
}

5
vendor/github.com/d5/tengo/compiler/ast/ast.go generated vendored Normal file
View File

@ -0,0 +1,5 @@
package ast
const (
nullRep = "<null>"
)

25
vendor/github.com/d5/tengo/compiler/ast/bad_expr.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
package ast
import "github.com/d5/tengo/compiler/source"
// BadExpr represents a bad expression.
type BadExpr struct {
From source.Pos
To source.Pos
}
func (e *BadExpr) exprNode() {}
// Pos returns the position of first character belonging to the node.
func (e *BadExpr) Pos() source.Pos {
return e.From
}
// End returns the position of first character immediately after the node.
func (e *BadExpr) End() source.Pos {
return e.To
}
func (e *BadExpr) String() string {
return "<bad expression>"
}

25
vendor/github.com/d5/tengo/compiler/ast/bad_stmt.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
package ast
import "github.com/d5/tengo/compiler/source"
// BadStmt represents a bad statement.
type BadStmt struct {
From source.Pos
To source.Pos
}
func (s *BadStmt) stmtNode() {}
// Pos returns the position of first character belonging to the node.
func (s *BadStmt) Pos() source.Pos {
return s.From
}
// End returns the position of first character immediately after the node.
func (s *BadStmt) End() source.Pos {
return s.To
}
func (s *BadStmt) String() string {
return "<bad statement>"
}

30
vendor/github.com/d5/tengo/compiler/ast/binary_expr.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
package ast
import (
"github.com/d5/tengo/compiler/source"
"github.com/d5/tengo/compiler/token"
)
// BinaryExpr represents a binary operator expression.
type BinaryExpr struct {
LHS Expr
RHS Expr
Token token.Token
TokenPos source.Pos
}
func (e *BinaryExpr) exprNode() {}
// Pos returns the position of first character belonging to the node.
func (e *BinaryExpr) Pos() source.Pos {
return e.LHS.Pos()
}
// End returns the position of first character immediately after the node.
func (e *BinaryExpr) End() source.Pos {
return e.RHS.End()
}
func (e *BinaryExpr) String() string {
return "(" + e.LHS.String() + " " + e.Token.String() + " " + e.RHS.String() + ")"
}

35
vendor/github.com/d5/tengo/compiler/ast/block_stmt.go generated vendored Normal file
View File

@ -0,0 +1,35 @@
package ast
import (
"strings"
"github.com/d5/tengo/compiler/source"
)
// BlockStmt represents a block statement.
type BlockStmt struct {
Stmts []Stmt
LBrace source.Pos
RBrace source.Pos
}
func (s *BlockStmt) stmtNode() {}
// Pos returns the position of first character belonging to the node.
func (s *BlockStmt) Pos() source.Pos {
return s.LBrace
}
// End returns the position of first character immediately after the node.
func (s *BlockStmt) End() source.Pos {
return s.RBrace + 1
}
func (s *BlockStmt) String() string {
var list []string
for _, e := range s.Stmts {
list = append(list, e.String())
}
return "{" + strings.Join(list, "; ") + "}"
}

26
vendor/github.com/d5/tengo/compiler/ast/bool_lit.go generated vendored Normal file
View File

@ -0,0 +1,26 @@
package ast
import "github.com/d5/tengo/compiler/source"
// BoolLit represents a boolean literal.
type BoolLit struct {
Value bool
ValuePos source.Pos
Literal string
}
func (e *BoolLit) exprNode() {}
// Pos returns the position of first character belonging to the node.
func (e *BoolLit) Pos() source.Pos {
return e.ValuePos
}
// End returns the position of first character immediately after the node.
func (e *BoolLit) End() source.Pos {
return source.Pos(int(e.ValuePos) + len(e.Literal))
}
func (e *BoolLit) String() string {
return e.Literal
}

38
vendor/github.com/d5/tengo/compiler/ast/branch_stmt.go generated vendored Normal file
View File

@ -0,0 +1,38 @@
package ast
import (
"github.com/d5/tengo/compiler/source"
"github.com/d5/tengo/compiler/token"
)
// BranchStmt represents a branch statement.
type BranchStmt struct {
Token token.Token
TokenPos source.Pos
Label *Ident
}
func (s *BranchStmt) stmtNode() {}
// Pos returns the position of first character belonging to the node.
func (s *BranchStmt) Pos() source.Pos {
return s.TokenPos
}
// End returns the position of first character immediately after the node.
func (s *BranchStmt) End() source.Pos {
if s.Label != nil {
return s.Label.End()
}
return source.Pos(int(s.TokenPos) + len(s.Token.String()))
}
func (s *BranchStmt) String() string {
var label string
if s.Label != nil {
label = " " + s.Label.Name
}
return s.Token.String() + label
}

36
vendor/github.com/d5/tengo/compiler/ast/call_expr.go generated vendored Normal file
View File

@ -0,0 +1,36 @@
package ast
import (
"strings"
"github.com/d5/tengo/compiler/source"
)
// CallExpr represents a function call expression.
type CallExpr struct {
Func Expr
LParen source.Pos
Args []Expr
RParen source.Pos
}
func (e *CallExpr) exprNode() {}
// Pos returns the position of first character belonging to the node.
func (e *CallExpr) Pos() source.Pos {
return e.Func.Pos()
}
// End returns the position of first character immediately after the node.
func (e *CallExpr) End() source.Pos {
return e.RParen + 1
}
func (e *CallExpr) String() string {
var args []string
for _, e := range e.Args {
args = append(args, e.String())
}
return e.Func.String() + "(" + strings.Join(args, ", ") + ")"
}

26
vendor/github.com/d5/tengo/compiler/ast/char_lit.go generated vendored Normal file
View File

@ -0,0 +1,26 @@
package ast
import "github.com/d5/tengo/compiler/source"
// CharLit represents a character literal.
type CharLit struct {
Value rune
ValuePos source.Pos
Literal string
}
func (e *CharLit) exprNode() {}
// Pos returns the position of first character belonging to the node.
func (e *CharLit) Pos() source.Pos {
return e.ValuePos
}
// End returns the position of first character immediately after the node.
func (e *CharLit) End() source.Pos {
return source.Pos(int(e.ValuePos) + len(e.Literal))
}
func (e *CharLit) String() string {
return e.Literal
}

30
vendor/github.com/d5/tengo/compiler/ast/cond_expr.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
package ast
import (
"github.com/d5/tengo/compiler/source"
)
// CondExpr represents a ternary conditional expression.
type CondExpr struct {
Cond Expr
True Expr
False Expr
QuestionPos source.Pos
ColonPos source.Pos
}
func (e *CondExpr) exprNode() {}
// Pos returns the position of first character belonging to the node.
func (e *CondExpr) Pos() source.Pos {
return e.Cond.Pos()
}
// End returns the position of first character immediately after the node.
func (e *CondExpr) End() source.Pos {
return e.False.End()
}
func (e *CondExpr) String() string {
return "(" + e.Cond.String() + " ? " + e.True.String() + " : " + e.False.String() + ")"
}

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