Compare commits

..

105 Commits

Author SHA1 Message Date
Wim
87788f354f Release v1.15.1 2019-07-15 23:09:46 +02:00
Wim
7d2e440c83 Add support for discord category channels (discord) (#863)
This adds support for the discord category option that can be used
to group channels in. This means we can have multiple channels with
the same name.

We add the option to specify a category in the channel option of a
discord account under [[gateway]]

Besides channel="channel" or channel="ID:channelID", now also
channel="category/channel" can be specified.

This change remains backwards compatible with people that haven't
specified the category and incorporates the fix in #861
2019-07-15 21:56:35 +02:00
Qais Patankar
5551f9d56f Fix discord channel & category name clash. #860 (#861) 2019-07-14 19:53:09 +02:00
Wim
1fb91c6316 Fix panic by checking slice bounds in handleEntities (telegram). Fixes #857 (#858)
Besides the bound checking, this now also use utf16 as suggested by
https://github.com/go-telegram-bot-api/telegram-bot-api/issues/231
2019-07-08 22:19:45 +02:00
Qais Patankar
e60949ff3f Support webhook message deletions (discord) (#853)
* Support webhook message deletions (discord)

Messages sent via webhook can now be deleted. It seems it can do this
without any special permissions.

This copies discordgo.WebhookExecute and makes it support the returning
of discordgo.Message.

A pull request has been sent upstream, so we should use that if
@bwmariin accepts the pull request:

https://github.com/bwmarrin/discordgo/pull/663

Changes in behaviour (webhook mode only):
- Previously messages *edited* on other platforms would just be
retransmitted as a brand new message to Discord.
- Message *edits* will now be ignored.
- Debug: message edits will now print out a "permission error".

In the future it may be good to send an "message edited" react to those
webhook messages, so at least people know that the message was edited on
other platforms. (Even though it can't actually show the new message.)

Alternatively, message edits could just send a brand new message with a
link back to the old one. This is a little ugly but it would ensure that
Discord users are able to see the edited message. These "message edit
notifications" would be sent from the bot user (not from a webhook), so
we could edit the "edit notification" if subsequent edits to the
original message are made.
2019-07-08 22:18:37 +02:00
Wim
278a3c6890 Update changelog 2019-06-30 18:50:45 +02:00
Wim
fcf734eb36 Update to golanci-lint v1.17.1 2019-06-30 18:43:54 +02:00
Wim
cf3cddafab Keep connection state. Fixes #856
Actually check if we're connected when trying to Send() a message.
Messages now will get dropped when not connected.

TODO: Ideally this should be in a ring buffer to retransmit when the
connection comes back up.
2019-06-30 18:34:41 +02:00
Wim
c52664f22e Update readme 2019-06-16 23:46:24 +02:00
Wim
cb712ff37d Update vendor (#852) 2019-06-16 23:33:25 +02:00
Qais Patankar
f4ae610448 Add .gitignore (#850) 2019-06-16 16:37:38 +02:00
Wim
601b8bc98d Update documentation and changelog 2019-06-16 16:32:12 +02:00
Joona Hoikkala
80b4cec87a Add an option to skip the Mattermost server version check (#849)
Adds SkipVersionCheck bool option for mattermost
2019-06-16 16:23:50 +02:00
Qais Patankar
76c7b69e4e Support bulk deletions (discord) 2019-06-16 16:07:48 +02:00
Wim
a5bd3c4dda Bump version 2019-06-16 16:02:41 +02:00
Wim
f06e9b5605 Release v1.15.0 2019-06-14 01:36:55 +02:00
Nick
7a3bb0e55c Verify TLS against JID domain, not the host. (xmpp) (#834)
Partially fixes #820.

A full fix requires patching https://github.com/matterbridge/go-xmpp to use DNS SRV records.
2019-06-14 01:10:43 +02:00
Wim
6e8f535e8b Fix logic (xmpp) 2019-06-14 00:44:31 +02:00
Wim
5619a75b05 Fix regression in autojoining with legacy tokens (slack). Fixes #651 (#848) 2019-06-14 00:42:55 +02:00
Wim
53dfb78215 Allow messages with timestamp (xmpp). Fixes #835 (#847) 2019-06-14 00:24:42 +02:00
Wim
8e97cbab1e Fix noisy whatsapp error logging 2019-06-14 00:02:32 +02:00
Wim
ce7b749fd5 Update github.com/Rhymen/go-whatsapp vendor. Fixes #843 2019-06-14 00:02:32 +02:00
Wim
6617bd6609 Revert xmpp to orig behaviour. Closes #844 2019-06-13 23:35:04 +02:00
Wim
e610fb3201 Make config parse errors readable 2019-06-02 09:35:20 +01:00
Wim
40f1d35415 Fix go mod issue by removing whatsapp-ext 2019-06-02 09:35:20 +01:00
Duco van Amstel
b79bf7d414 Forward only user-typing messages if supported by protocol (#832)
Fixes issue #814.

This is a somewhat hacky way of achieving the required goal but it seems
like this is the least problematic way of getting there.

We might want to redesign some bridge information later such that we
have a standardised way of specifying what is and what isn't supported
by each chat protocol / bridge.
2019-05-30 15:00:40 +02:00
Duco van Amstel
3724cc3a15 Clean-up XMPP handling code (#831) 2019-05-30 12:31:54 +02:00
Wim
3418e8c9af Use upstream whatsapp again (#809) 2019-05-30 12:20:56 +02:00
Duco van Amstel
9619dff334 Linter fixes 2019-05-27 17:38:31 +01:00
Wim
1b2feb19e5 Update channels of all teams (mattermost) 2019-05-02 00:46:49 +02:00
Wim
1829dc3d9f Allow messages from other bots (discord). Fixes #816 2019-05-01 18:10:31 +02:00
Wim
bd0e81f5a0 Add msg event to tengo 2019-04-24 22:47:37 +02:00
Wim
f04d360ee2 Update README with v1.14.4 2019-04-23 23:36:30 +02:00
Wim
92f27281fa Update changelog 2019-04-23 23:35:48 +02:00
Wim
65781b9316 Disable user lookups on delete messages (slack) (#812) 2019-04-23 23:29:15 +02:00
Duco van Amstel
9be0be0316 Add lacking clean-up in Slack synchronisation (#811) 2019-04-23 23:08:34 +02:00
Wim
9f5f004725 Use paging in initUser and UpdateUsers (mattermost) 2019-04-20 23:06:06 +02:00
Wim
fed77cccf3 Handle unthreaded messages (mattermost). Fixes #803 2019-04-19 23:31:45 +02:00
Wim
9b520dfb78 Fix panic on nil message.Post (mattermost). Fixes #804 2019-04-19 23:08:41 +02:00
Wim
8ad2be10b2 Add Id to EditMessage (mattermost). Fixes #802 2019-04-19 22:59:04 +02:00
Wim
2d277a15f5 Add scripting (tengo) support for every outgoing message (#806)
Adds a new key OutMessage under [tengo] table, which specifies the location of the script that
will be invoked on each message being sent to a bridge and can be used to modify the Username
and the Text of that message.

The script will have the following global variables:
read-only:
inAccount, inProtocol, inChannel, inGateway
outAccount, outProtocol, outChannel, outGateway

read-write:
msgText, msgUsername

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

The default script in https://github.com/42wim/matterbridge/tree/master/internal/tengo/outmessage.tengo
is compiled in and will be executed if no script is specified.
2019-04-19 18:27:31 +02:00
Wim
d60468bb05 Bump version 2019-04-19 18:24:13 +02:00
Wim
82d6210464 Update changelog 2019-04-19 18:23:50 +02:00
Wim
ff198042d2 Remove deprecated TengoModifyMessage
This has become InMessage under [tengo]
2019-04-19 00:13:28 +02:00
chotaire
6b47e29583 Add verbose IRC joins/parts (ident@host) (#805)
New configuration setting: VerboseJoinPart (default is false)
2019-04-18 23:56:05 +02:00
Wim
380c38674c Fix deadlock on reconnect (irc). Closes #757 2019-04-15 23:28:47 +02:00
Wim
3c14a0891e Remove hipchat 2019-04-14 23:54:05 +02:00
Wim
8513a07416 Update README 2019-04-14 23:48:54 +02:00
Qais Patankar
220485a849 Add remotenickformat-zerowidth.tengo to contrib (#799) 2019-04-14 23:42:16 +02:00
Wim
4db34b0506 Send channel_created and deleted event through message channel (mattermost) 2019-04-13 21:52:39 +02:00
Wim
5677c912a8 Add useraction support (rocketchat). Closes #772 (#794) 2019-04-08 23:30:22 +02:00
Wim
7a24de15e4 Add tengo support to RemoteNickFormat (#793)
This commit add support for using the result of a tengo script in RemoteNickFormat using {TENGO}
Also adds a new toml table [tengo] with key RemoteNickFormat and value location of the script.
This also moves the TengoModifyMessage from [general] to Message in [tengo]

Documentation:

RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script.
The script will have the following global variables:
to modify: result
to read: channel, bridge, gateway, protocol, nick

The result will be set in {TENGO} in the RemoteNickFormat key of every bridge where {TENGO} is specified
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/contrib/remotenickformat.tengo

[tengo]
RemoteNickFormat="remotenickformat.tengo"
2019-04-08 20:58:21 +02:00
Wim
99d9ea283a Build on every branch (travis) 2019-04-07 23:57:47 +02:00
Wim
dac92a0e0a Add xmpp room to README. Closes #758 2019-04-07 15:48:19 +02:00
Wim
a25efb16f3 Bump version 2019-04-07 15:41:14 +02:00
Wim
e4d73b29a1 Release v1.14.2 2019-04-06 23:29:49 +02:00
Wim
8a875f292e Revert fix for #722. Closes #781
Revert "Fix typo"

This reverts commit dffd67eb31.

Revert "Handle quit message relay better on gateways with one channel on the irc bridge #722"

This reverts commit 240559581a.

Revert "Support quits from irc correctly. Fixes #722 (#724)"

This reverts commit d76a04bd0a.
2019-04-06 23:12:48 +02:00
Wim
60a85621ea Return when not connected and drop a message (irc). Fixes #786 2019-04-06 22:34:41 +02:00
Wim
115d20373c Update tengo vendor and load the stdlib. Fixes #789 (#792) 2019-04-06 22:18:25 +02:00
Wim
cdf33e5748 Use default nick if none specified (irc). Fixes #785 2019-04-05 00:17:46 +02:00
Wim
01d0a9f412 Handle nil message (telegram). Fixes #777 2019-04-05 00:04:08 +02:00
Wim
8cc2d3b4fe Revert "Bail if any vars are nil, not if all (telegram) (#778)"
This reverts commit efd2c99862.
2019-04-05 00:02:26 +02:00
Wim
aba9e4f3be Fix travis before_deploy 2019-04-04 23:22:56 +02:00
Wim
4d575ba13a Fix travis deploy condition and update to golangci-lint v1.16 2019-04-04 23:05:58 +02:00
Duco van Amstel
7f0e4ad448 Add CI fixes and improvements (#780)
* Update GolangCI-lint and lint config

The `algo` parameter for the `unparam` linter has been removed and we
should thus no longer specify it. Also, bumping the GolangCI-lint
version to the latest available minor release.

See: mvdan/unparam@e6a6d1c51b

* Fix and improve bintray CI script

* Further CI setup improvements

* Split-out CI steps into stand-alone scripts
2019-04-04 22:54:51 +02:00
Wim
17cc14a9d2 Send user_added and removed event through message channel (mattermost) 2019-04-02 00:15:58 +02:00
Wim
1f8016182c Return channelId for other channeltypes too (mattermost) 2019-04-01 22:50:19 +02:00
Wim
caf9ef2c4b Bump travis to go 1.12.x 2019-03-27 23:22:55 +01:00
Wim
64b57f2da3 Ignore message_replied and hidden messages (slack). Fixes #709 (#779) 2019-03-27 22:54:18 +01:00
David Hill
efd2c99862 Bail if any vars are nil, not if all (telegram) (#778) 2019-03-27 21:00:57 +01:00
Wim
cc05ba8907 Thank DigitalOcean (https://digitalocean.com) for another year of sponsorship 2019-03-25 21:13:11 +01:00
Wim
16763b715a Look up #channel too (rocketchat). Fix #773 (#775) 2019-03-24 20:15:15 +01:00
Wim
ffaa598796 Bump version 2019-03-24 19:52:31 +01:00
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
David Hill
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
Duco van Amstel
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
Duco van Amstel
fb2667631d Refactor channel and user management (slack) (#766) 2019-03-15 21:23:09 +01:00
Duco van Amstel
b638f7037a Force Slack link unfurling (#763) 2019-03-12 22:56:43 +01:00
Duco van Amstel
74699a8262 Split-out Slack user and channel management (#762) 2019-03-12 22:52:36 +01:00
Duco van Amstel
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
518 changed files with 87465 additions and 15147 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# Exclude matterbridge binary
matterbridge
# Exclude configuration file
matterbridge.toml

View File

@@ -7,7 +7,7 @@ run:
# concurrency: 4 # concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m # timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 1m deadline: 2m
# exit code when at least one issue was found, default is 1 # exit code when at least one issue was found, default is 1
issues-exit-code: 1 issues-exit-code: 1
@@ -105,10 +105,6 @@ linters-settings:
# with golangci-lint call it on a directory with the changed file. # with golangci-lint call it on a directory with the changed file.
check-exported: false check-exported: false
unparam: 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. # 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: # 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 # if it's called for subdir of a project it can't find external interfaces. All text editor integrations
@@ -158,7 +154,6 @@ linters-settings:
- regexpMust - regexpMust
- singleCaseSwitch - singleCaseSwitch
- sloppyLen - sloppyLen
- sloppyReassign
- switchTrue - switchTrue
- typeSwitchVar - typeSwitchVar
- typeUnparen - typeUnparen

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,57 +1,56 @@
language: go language: go
go:
- 1.11.x
go_import_path: github.com/42wim/matterbridge go_import_path: github.com/42wim/matterbridge
# we have everything vendored # We have everything vendored so this helps TravisCI not run `go get ...`.
install: true install: true
git: git:
depth: 200 depth: 200
env:
global:
- GOOS=linux GOARCH=amd64
- GOLANGCI_VERSION="v1.14.0"
matrix:
# It's ok if our code fails on unstable development versions of Go.
allow_failures:
- go: tip
# Don't wait for tip tests to finish. Mark the test run green if the
# tests pass on the stable versions of Go.
fast_finish: true
notifications: notifications:
email: false email: false
before_script: branches:
# Get version info from tags. only:
- MY_VERSION="$(git describe --tags)" - master
# 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
script: jobs:
# Run the linter. include:
- golangci-lint run - stage: lint
# Run all the tests with the race detector and generate coverage. # Run linting in one Go environment only.
- go test -v -race -coverprofile c.out ./... script: ./ci/lint.sh
# Run the build script to generate the necessary binaries and images. go: 1.12.x
- /bin/bash ci/bintray.sh env:
- GO111MODULE=on
- GOLANGCI_VERSION="v1.17.1"
- stage: test
# Run tests in a combination of Go environments.
script: ./ci/test.sh
go: 1.11.x
env:
- GO111MODULE=off
- script: ./ci/test.sh
go: 1.11.x
env:
- GO111MODULE=on
- script: ./ci/test.sh
go: 1.12.x
env:
- GO111MODULE=on
- REPORT_COVERAGE=1
- BINDEPLOY=1
after_script: before_deploy: /bin/bash ci/bintray.sh
# Upload test coverage to CodeClimate.
- ./cc-test-reporter after-build --exit-code ${TRAVIS_TEST_RESULT}
deploy: deploy:
on:
all_branches: true
condition: $BINDEPLOY = 1
provider: bintray provider: bintray
edge: edge:
branch: v1.8.47 branch: v1.8.47
file: ci/deploy.json file: ci/deploy.json
user: 42wim user: 42wim
key: key:
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI=" secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="

View File

@@ -35,6 +35,12 @@
**Note:** Matter<em>most</em> isn't required to run matter<em>bridge</em>.</sup></div> **Note:** Matter<em>most</em> isn't required to run matter<em>bridge</em>.</sup></div>
<p>
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="201px">
</a>
</p>
### Table of Contents ### Table of Contents
* [Features](https://github.com/42wim/matterbridge/wiki/Features) * [Features](https://github.com/42wim/matterbridge/wiki/Features)
* [Natively supported](#natively-supported) * [Natively supported](#natively-supported)
@@ -42,11 +48,12 @@
* [API](#API) * [API](#API)
* [Chat with us](#chat-with-us) * [Chat with us](#chat-with-us)
* [Screenshots](https://github.com/42wim/matterbridge/wiki/) * [Screenshots](https://github.com/42wim/matterbridge/wiki/)
* [Installing](#installing) * [Installing/upgrading](#installing--upgrading)
* [Binaries](#binaries) * [Binaries](#binaries)
* [Building](#building) * [Building](#building)
* [Configuration](#configuration) * [Configuration](#configuration)
* [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) * [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config)
* [Settings](#settings)
* [Examples](#examples) * [Examples](#examples)
* [Running](#running) * [Running](#running)
* [Docker](#docker) * [Docker](#docker)
@@ -75,7 +82,6 @@
* [Slack](https://slack.com) * [Slack](https://slack.com)
* [Discord](https://discordapp.com) * [Discord](https://discordapp.com)
* [Telegram](https://telegram.org) * [Telegram](https://telegram.org)
* [Hipchat](https://www.hipchat.com)
* [Rocket.chat](https://rocket.chat) * [Rocket.chat](https://rocket.chat)
* [Matrix](https://matrix.org) * [Matrix](https://matrix.org)
* [Steam](https://store.steampowered.com/) * [Steam](https://store.steampowered.com/)
@@ -88,9 +94,10 @@
* [Minecraft](https://github.com/elytra/MatterLink) * [Minecraft](https://github.com/elytra/MatterLink)
* [Reddit](https://github.com/bonehurtingjuice/mattereddit) * [Reddit](https://github.com/bonehurtingjuice/mattereddit)
* [Facebook messenger](https://github.com/VictorNine/fbridge) * [Facebook messenger](https://github.com/VictorNine/fbridge)
* [Discourse](https://github.com/DeclanHoare/matterbabble)
### API ### API
The API is very basic at the moment. The API is basic at the moment.
More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api). 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. Used by the projects below. Feel free to make a PR to add your project to this list.
@@ -99,6 +106,7 @@ Used by the projects below. Feel free to make a PR to add your project to this l
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot) * [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
* [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support) * [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) * [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
* [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
## Chat with us ## Chat with us
@@ -111,7 +119,7 @@ Questions or want to test on your favorite platform? Join below:
* [Slack][mb-slack] * [Slack][mb-slack]
* [Mattermost][mb-mattermost] * [Mattermost][mb-mattermost]
* [Rocket.Chat][mb-rocketchat] * [Rocket.Chat][mb-rocketchat]
* [XMPP][mb-xmpp] * [XMPP][mb-xmpp] (matterbridge@conference.jabber.de)
* [Twitch][mb-twitch] * [Twitch][mb-twitch]
* [Zulip][mb-zulip] * [Zulip][mb-zulip]
* [Telegram][mb-telegram] * [Telegram][mb-telegram]
@@ -119,15 +127,20 @@ Questions or want to test on your favorite platform? Join below:
## Screenshots ## Screenshots
See https://github.com/42wim/matterbridge/wiki See https://github.com/42wim/matterbridge/wiki
## Installing ## Installing / upgrading
### Binaries ### Binaries
* Latest stable release [v1.13.1](https://github.com/42wim/matterbridge/releases/latest) * Latest stable release [v1.15.1](https://github.com/42wim/matterbridge/releases/latest)
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/) * Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest) and follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
### Packages ### Packages
* [Overview](https://repology.org/metapackage/matterbridge/versions) * [Overview](https://repology.org/metapackage/matterbridge/versions)
### Building ## Building
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest)
If you really want to build from source, follow these instructions:
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). 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. After Go is setup, download matterbridge to your $GOPATH directory.
@@ -148,6 +161,9 @@ matterbridge
### Basic configuration ### Basic configuration
See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
### Settings
All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for each bridge.
### Advanced configuration ### Advanced configuration
* [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example. * [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
@@ -237,10 +253,6 @@ See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.m
See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
Want to tip ?
* eth: 0xb3f9b5387c66ad6be892bcb7bbc67862f3abc16f
* btc: 1N7cKHj5SfqBHBzDJ6kad4BzeqUBBS2zhs
## Related projects ## Related projects
* [FOSSRIT/infrastructure - roles/matterbridge](https://github.com/FOSSRIT/infrastructure/tree/master/roles/matterbridge) (Ansible role used to automate deployments of Matterbridge) * [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) * [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig)
@@ -251,6 +263,8 @@ Want to tip ?
* [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost * [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot) * [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) * [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 ## Articles
* [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3) * [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
@@ -264,7 +278,13 @@ Want to tip ?
* https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/ * https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/
## Thanks ## Thanks
[![Digitalocean](https://snag.gy/3LVifX.jpg)](https://www.digitalocean.com/) for sponsoring demo/testing droplets.
<p>This project is supported by:</p>
<p>
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
</a>
</p>
Matterbridge wouldn't exist without these libraries: Matterbridge wouldn't exist without these libraries:
* discord - https://github.com/bwmarrin/discordgo * discord - https://github.com/bwmarrin/discordgo

View File

@@ -93,6 +93,7 @@ type Protocol struct {
MediaDownloadSize int // all protocols MediaDownloadSize int // all protocols
MediaServerDownload string MediaServerDownload string
MediaServerUpload string MediaServerUpload string
MediaConvertWebPToPNG bool // telegram
MessageDelay int // IRC, time in millisecond to wait between messages MessageDelay int // IRC, time in millisecond to wait between messages
MessageFormat string // telegram MessageFormat string // telegram
MessageLength int // IRC, max length of a message allowed MessageLength int // IRC, max length of a message allowed
@@ -119,13 +120,14 @@ type Protocol struct {
ReplaceMessages [][]string // all protocols ReplaceMessages [][]string // all protocols
ReplaceNicks [][]string // all protocols ReplaceNicks [][]string // all protocols
RemoteNickFormat string // all protocols RemoteNickFormat string // all protocols
RunCommands []string // irc RunCommands []string // IRC
Server string // IRC,mattermost,XMPP,discord Server string // IRC,mattermost,XMPP,discord
ShowJoinPart bool // all protocols ShowJoinPart bool // all protocols
ShowTopicChange bool // slack ShowTopicChange bool // slack
ShowUserTyping bool // slack ShowUserTyping bool // slack
ShowEmbeds bool // discord ShowEmbeds bool // discord
SkipTLSVerify bool // IRC, mattermost SkipTLSVerify bool // IRC, mattermost
SkipVersionCheck bool // mattermost
StripNick bool // all protocols StripNick bool // all protocols
SyncTopic bool // slack SyncTopic bool // slack
TengoModifyMessage string // general TengoModifyMessage string // general
@@ -140,6 +142,7 @@ type Protocol struct {
UseFirstName bool // telegram UseFirstName bool // telegram
UseUserName bool // discord UseUserName bool // discord
UseInsecureURL bool // telegram UseInsecureURL bool // telegram
VerboseJoinPart bool // IRC
WebhookBindAddress string // mattermost, slack WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack WebhookURL string // mattermost, slack
} }
@@ -165,6 +168,13 @@ type Gateway struct {
InOut []Bridge InOut []Bridge
} }
type Tengo struct {
InMessage string
Message string
RemoteNickFormat string
OutMessage string
}
type SameChannelGateway struct { type SameChannelGateway struct {
Name string Name string
Enable bool Enable bool
@@ -189,6 +199,7 @@ type BridgeValues struct {
WhatsApp map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results WhatsApp map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results
Zulip map[string]Protocol Zulip map[string]Protocol
General Protocol General Protocol
Tengo Tengo
Gateway []Gateway Gateway []Gateway
SameChannelGateway []SameChannelGateway SameChannelGateway []SameChannelGateway
} }
@@ -244,12 +255,12 @@ func newConfigFromString(logger *logrus.Entry, input []byte) *config {
viper.AutomaticEnv() viper.AutomaticEnv()
if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil { if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil {
logger.Fatalf("Failed to parse the configuration: %#v", err) logger.Fatalf("Failed to parse the configuration: %s", err)
} }
cfg := &BridgeValues{} cfg := &BridgeValues{}
if err := viper.Unmarshal(cfg); err != nil { if err := viper.Unmarshal(cfg); err != nil {
logger.Fatalf("Failed to load the configuration: %#v", err) logger.Fatalf("Failed to load the configuration: %s", err)
} }
return &config{ return &config{
logger: logger, logger: logger,

View File

@@ -75,6 +75,7 @@ func (b *Bdiscord) Connect() error {
b.c.AddHandler(b.memberUpdate) b.c.AddHandler(b.memberUpdate)
b.c.AddHandler(b.messageUpdate) b.c.AddHandler(b.messageUpdate)
b.c.AddHandler(b.messageDelete) b.c.AddHandler(b.messageDelete)
b.c.AddHandler(b.messageDeleteBulk)
b.c.AddHandler(b.memberAdd) b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove) b.c.AddHandler(b.memberRemove)
err = b.c.Open() err = b.c.Open()
@@ -95,11 +96,11 @@ func (b *Bdiscord) Connect() error {
for _, guild := range guilds { for _, guild := range guilds {
if guild.Name == serverName || guild.ID == serverName { if guild.Name == serverName || guild.ID == serverName {
b.channels, err = b.c.GuildChannels(guild.ID) b.channels, err = b.c.GuildChannels(guild.ID)
b.guildID = guild.ID
guildFound = true
if err != nil { if err != nil {
break break
} }
b.guildID = guild.ID
guildFound = true
} }
} }
b.channelsMutex.Unlock() b.channelsMutex.Unlock()
@@ -208,11 +209,21 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
b.channelsMutex.RUnlock() b.channelsMutex.RUnlock()
// Use webhook to send the message // Use webhook to send the message
if wID != "" { if wID != "" && msg.Event != config.EventMsgDelete {
// skip events // skip events
if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange { if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
return "", nil return "", nil
} }
// If we are editing a message, delete the old message
if msg.ID != "" {
b.Log.Debugf("Deleting edited webhook message")
err := b.c.ChannelMessageDelete(channelID, msg.ID)
if err != nil {
b.Log.Errorf("Could not delete edited webhook message: %s", err)
}
}
b.Log.Debugf("Broadcasting using Webhook") b.Log.Debugf("Broadcasting using Webhook")
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
@@ -246,11 +257,11 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel) b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel)
_, err := b.c.WebhookEdit(wID, "", "", channelID) _, err := b.c.WebhookEdit(wID, "", "", channelID)
if err != nil { if err != nil {
b.Log.Errorf("Could not set webhook channel: %v", err) b.Log.Errorf("Could not set webhook channel: %s", err)
return "", err return "", err
} }
} }
err := b.c.WebhookExecute( msg, err := b.webhookExecute(
wID, wID,
wToken, wToken,
true, true,
@@ -259,7 +270,7 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
Username: msg.Username, Username: msg.Username,
AvatarURL: msg.Avatar, AvatarURL: msg.Avatar,
}) })
return "", err return msg.ID, err
} }
b.Log.Debugf("Broadcasting using token (API)") b.Log.Debugf("Broadcasting using token (API)")
@@ -278,7 +289,7 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { for _, rmsg := range helper.HandleExtra(&msg, b.General) {
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength) rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength)
if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil { if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
b.Log.Errorf("Could not send message %#v: %v", rmsg, err) b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
} }
} }
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
@@ -360,7 +371,7 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
} }
_, err = b.c.ChannelMessageSendComplex(channelID, &m) _, err = b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil { if err != nil {
return "", fmt.Errorf("file upload failed: %#v", err) return "", fmt.Errorf("file upload failed: %s", err)
} }
} }
return "", nil return "", nil

View File

@@ -16,6 +16,27 @@ func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelet
b.Remote <- rmsg b.Remote <- rmsg
} }
// TODO(qaisjp): if other bridges support bulk deletions, it could be fanned out centrally
func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageDeleteBulk) { //nolint:unparam
for _, msgID := range m.Messages {
rmsg := config.Message{
Account: b.Account,
ID: msgID,
Event: config.EventMsgDelete,
Text: config.EventMsgDelete,
Channel: "ID:" + m.ChannelID,
}
if !b.useChannelID {
rmsg.Channel = b.getChannelName(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 func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam
if b.GetBool("EditDisable") { if b.GetBool("EditDisable") {
return return

View File

@@ -1,6 +1,7 @@
package bdiscord package bdiscord
import ( import (
"encoding/json"
"errors" "errors"
"regexp" "regexp"
"strings" "strings"
@@ -25,7 +26,7 @@ func (b *Bdiscord) getNick(user *discordgo.User) string {
// If we didn't find nick, search for it. // If we didn't find nick, search for it.
member, err := b.c.GuildMember(b.guildID, user.ID) member, err := b.c.GuildMember(b.guildID, user.ID)
if err != nil { if err != nil {
b.Log.Warnf("Failed to fetch information for member %#v: %#v", user, err) b.Log.Warnf("Failed to fetch information for member %#v: %s", user, err)
return user.Username return user.Username
} else if member == nil { } else if member == nil {
b.Log.Warnf("Got no information for member %#v", user) b.Log.Warnf("Got no information for member %#v", user)
@@ -51,6 +52,9 @@ func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error)
} }
func (b *Bdiscord) getChannelID(name string) string { func (b *Bdiscord) getChannelID(name string) string {
if strings.Contains(name, "/") {
return b.getCategoryChannelID(name)
}
b.channelsMutex.RLock() b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock() defer b.channelsMutex.RUnlock()
@@ -59,25 +63,70 @@ func (b *Bdiscord) getChannelID(name string) string {
return idcheck[1] return idcheck[1]
} }
for _, channel := range b.channels { for _, channel := range b.channels {
if channel.Name == name { if channel.Name == name && channel.Type == discordgo.ChannelTypeGuildText {
return channel.ID return channel.ID
} }
} }
return "" return ""
} }
func (b *Bdiscord) getCategoryChannelID(name string) string {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
res := strings.Split(name, "/")
// shouldn't happen because function should be only called from getChannelID
if len(res) != 2 {
return ""
}
catName, chanName := res[0], res[1]
for _, channel := range b.channels {
// if we have a parentID, lookup the name of that parent (category)
// and if it matches return it
if channel.Name == chanName && channel.ParentID != "" {
for _, cat := range b.channels {
if cat.ID == channel.ParentID && cat.Name == catName {
return channel.ID
}
}
}
}
return ""
}
func (b *Bdiscord) getChannelName(id string) string { func (b *Bdiscord) getChannelName(id string) string {
b.channelsMutex.RLock() b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock() defer b.channelsMutex.RUnlock()
for _, channel := range b.channels { for _, channel := range b.channels {
if channel.ID == id { if channel.ID == id {
return channel.Name return b.getCategoryChannelName(channel.Name, channel.ParentID)
} }
} }
return "" return ""
} }
func (b *Bdiscord) getCategoryChannelName(name, parentID string) string {
var usesCat bool
// do we have a category configuration in the channel config
for _, c := range b.channelInfoMap {
if strings.Contains(c.Name, "/") {
usesCat = true
break
}
}
// configuration without category, return the normal channel name
if !usesCat {
return name
}
// create a category/channel response
for _, c := range b.channels {
if c.ID == parentID {
name = c.Name + "/" + name
}
}
return name
}
var ( var (
// See https://discordapp.com/developers/docs/reference#message-formatting. // See https://discordapp.com/developers/docs/reference#message-formatting.
channelMentionRE = regexp.MustCompile("<#[0-9]+>") channelMentionRE = regexp.MustCompile("<#[0-9]+>")
@@ -87,12 +136,12 @@ var (
func (b *Bdiscord) replaceChannelMentions(text string) string { func (b *Bdiscord) replaceChannelMentions(text string) string {
replaceChannelMentionFunc := func(match string) string { replaceChannelMentionFunc := func(match string) string {
var err error
channelID := match[2 : len(match)-1] channelID := match[2 : len(match)-1]
channelName := b.getChannelName(channelID) channelName := b.getChannelName(channelID)
// If we don't have the channel refresh our list. // If we don't have the channel refresh our list.
if channelName == "" { if channelName == "" {
var err error
b.channels, err = b.c.GuildChannels(b.guildID) b.channels, err = b.c.GuildChannels(b.guildID)
if err != nil { if err != nil {
return "#unknownchannel" return "#unknownchannel"
@@ -187,3 +236,26 @@ func enumerateUsernames(s string) []string {
} }
return usernames return usernames
} }
// webhookExecute executes a webhook.
// webhookID: The ID of a webhook.
// token : The auth token for the webhook
// wait : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise)
func (b *Bdiscord) webhookExecute(webhookID, token string, wait bool, data *discordgo.WebhookParams) (st *discordgo.Message, err error) {
uri := discordgo.EndpointWebhookToken(webhookID, token)
if wait {
uri += "?wait=true"
}
response, err := b.c.RequestWithBucketID("POST", uri, data, discordgo.EndpointWebhookToken("", ""))
if !wait || err != nil {
return nil, err
}
err = json.Unmarshal(response, &st)
if err != nil {
return nil, discordgo.ErrJSONUnmarshal
}
return st, nil
}

View File

@@ -3,6 +3,7 @@ package helper
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"image/png"
"io" "io"
"net/http" "net/http"
"regexp" "regexp"
@@ -10,6 +11,8 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"golang.org/x/image/webp"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gitlab.com/golang-commonmark/markdown" "gitlab.com/golang-commonmark/markdown"
@@ -175,5 +178,24 @@ func ClipMessage(text string, length int) string {
func ParseMarkdown(input string) string { func ParseMarkdown(input string) string {
md := markdown.New(markdown.XHTMLOutput(true), markdown.Breaks(true)) md := markdown.New(markdown.XHTMLOutput(true), markdown.Breaks(true))
return (md.RenderToString([]byte(input))) 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 package helper
import ( import (
"io/ioutil"
"os"
"testing" "testing"
"github.com/stretchr/testify/assert" "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) 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()
}
}

View File

@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -91,12 +90,13 @@ func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
if b.GetBool("nosendjoinpart") { if b.GetBool("nosendjoinpart") {
return 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} msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
if b.GetBool("verbosejoinpart") {
b.Log.Debugf("<= Sending verbose JOIN_LEAVE event from %s to gateway", b.Account)
msg = config.Message{Username: "system", Text: event.Source.Name + " (" + event.Source.Ident + "@" + event.Source.Host + ") " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
} else {
b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account)
}
b.Log.Debugf("<= Message is %#v", msg) b.Log.Debugf("<= Message is %#v", msg)
b.Remote <- msg b.Remote <- msg
return return
@@ -160,7 +160,10 @@ func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
b.handleNickServ() b.handleNickServ()
b.handleRunCommands() b.handleRunCommands()
// we are now fully connected // we are now fully connected
b.connected <- nil // only send on first connection
if b.FirstConnection {
b.connected <- nil
}
} }
func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
@@ -178,10 +181,6 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
// strip action, we made an event if it was an action // strip action, we made an event if it was an action
rmsg.Text += event.StripAction() 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 // start detecting the charset
mycharset := b.GetString("Charset") mycharset := b.GetString("Charset")
if mycharset == "" { if mycharset == "" {

View File

@@ -137,6 +137,7 @@ func (b *Birc) Send(msg config.Message) (string, error) {
// we can be in between reconnects #385 // we can be in between reconnects #385
if !b.i.IsConnected() { if !b.i.IsConnected() {
b.Log.Error("Not connected to server, dropping message") b.Log.Error("Not connected to server, dropping message")
return "", nil
} }
// Execute a command // Execute a command
@@ -231,7 +232,7 @@ func (b *Birc) getClient() (*girc.Client, error) {
// fix strict user handling of girc // fix strict user handling of girc
user := b.GetString("Nick") user := b.GetString("Nick")
for !girc.IsValidUser(user) { for !girc.IsValidUser(user) {
if len(user) == 1 { if len(user) == 1 || len(user) == 0 {
user = "matterbridge" user = "matterbridge"
break break
} }

View File

@@ -70,6 +70,7 @@ func (b *Bmattermost) apiLogin() error {
b.mc.SetLogLevel("debug") b.mc.SetLogLevel("debug")
} }
b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify") b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify")
b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck")
b.mc.NoTLS = b.GetBool("NoTLS") b.mc.NoTLS = b.GetBool("NoTLS")
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server")) b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
err := b.mc.Login() err := b.mc.Login()
@@ -186,6 +187,12 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
return true return true
} }
// Ignore non-post messages
if message.Post == nil {
b.Log.Debugf("ignoring nil message.Post: %#v", message)
return true
}
// Ignore messages sent from matterbridge // Ignore messages sent from matterbridge
if message.Post.Props != nil { if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok { if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {

View File

@@ -121,6 +121,12 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
return msg.ID, b.mc.DeleteMessage(msg.ID) return msg.ID, b.mc.DeleteMessage(msg.ID)
} }
// Handle prefix hint for unthreaded messages.
if msg.ParentID == "msg-parent-not-found" {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
// Upload a file if it exists // Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { for _, rmsg := range helper.HandleExtra(&msg, b.General) {

View File

@@ -40,6 +40,11 @@ func (b *Brocketchat) handleRocketHook(messages chan *config.Message) {
func (b *Brocketchat) handleRocketClient(messages chan *config.Message) { func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
for message := range b.messageChan { 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) b.Log.Debugf("message %#v", message)
m := message m := message
if b.skipMessage(&m) { if b.skipMessage(&m) {

View File

@@ -95,7 +95,7 @@ func (b *Brocketchat) getChannelID(name string) string {
b.RLock() b.RLock()
defer b.RUnlock() defer b.RUnlock()
for k, v := range b.channelMap { for k, v := range b.channelMap {
if v == name { if v == name || v == "#"+name {
return k return k
} }
} }

View File

@@ -2,6 +2,7 @@ package brocketchat
import ( import (
"errors" "errors"
"strings"
"sync" "sync"
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
@@ -9,16 +10,18 @@ import (
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/hook/rockethook" "github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
lru "github.com/hashicorp/golang-lru"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models" "github.com/matterbridge/Rocket.Chat.Go.SDK/models"
"github.com/matterbridge/Rocket.Chat.Go.SDK/realtime" "github.com/matterbridge/Rocket.Chat.Go.SDK/realtime"
"github.com/matterbridge/Rocket.Chat.Go.SDK/rest" "github.com/matterbridge/Rocket.Chat.Go.SDK/rest"
) )
type Brocketchat struct { type Brocketchat struct {
mh *matterhook.Client mh *matterhook.Client
rh *rockethook.Client rh *rockethook.Client
c *realtime.Client c *realtime.Client
r *rest.Client r *rest.Client
cache *lru.Cache
*bridge.Config *bridge.Config
messageChan chan models.Message messageChan chan models.Message
channelMap map[string]string channelMap map[string]string
@@ -27,9 +30,16 @@ type Brocketchat struct {
} }
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
b := &Brocketchat{Config: cfg} newCache, err := lru.New(100)
b.messageChan = make(chan models.Message) if err != nil {
b.channelMap = make(map[string]string) 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") b.Log.Debugf("enabling rocketchat")
return b return b
} }
@@ -76,14 +86,14 @@ func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
if b.c == nil { if b.c == nil {
return nil return nil
} }
id, err := b.c.GetChannelId(channel.Name) id, err := b.c.GetChannelId(strings.TrimPrefix(channel.Name, "#"))
if err != nil { if err != nil {
return err return err
} }
b.Lock() b.Lock()
b.channelMap[id] = channel.Name b.channelMap[id] = channel.Name
b.Unlock() b.Unlock()
mychannel := &models.Channel{ID: id, Name: channel.Name} mychannel := &models.Channel{ID: id, Name: strings.TrimPrefix(channel.Name, "#")}
if err := b.c.JoinChannel(id); err != nil { if err := b.c.JoinChannel(id); err != nil {
return err return err
} }
@@ -94,8 +104,15 @@ func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
} }
func (b *Brocketchat) Send(msg config.Message) (string, error) { func (b *Brocketchat) Send(msg config.Message) (string, error) {
// strip the # if people has set this
msg.Channel = strings.TrimPrefix(msg.Channel, "#")
channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel} channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel}
// Make a action /me of the message
if msg.Event == config.EventUserAction {
msg.Text = "_" + msg.Text + "_"
}
// Delete message // Delete message
if msg.Event == config.EventMsgDelete { if msg.Event == config.EventMsgDelete {
if msg.ID == "" { if msg.ID == "" {
@@ -122,6 +139,8 @@ func (b *Brocketchat) Send(msg config.Message) (string, error) {
// Upload a file if it exists // Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { for _, rmsg := range helper.HandleExtra(&msg, b.General) {
// strip the # if people has set this
rmsg.Channel = strings.TrimPrefix(rmsg.Channel, "#")
smsg := &models.Message{ smsg := &models.Message{
RoomID: b.getChannelID(rmsg.Channel), RoomID: b.getChannelID(rmsg.Channel),
Msg: rmsg.Username + rmsg.Text, Msg: rmsg.Username + rmsg.Text,

View File

@@ -22,20 +22,20 @@ func (b *Bslack) handleSlack() {
time.Sleep(time.Second) time.Sleep(time.Second)
b.Log.Debug("Start listening for Slack messages") b.Log.Debug("Start listening for Slack messages")
for message := range messages { for message := range messages {
if message.Event != config.EventUserTyping { // don't do any action on deleted/typing messages
if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete {
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
// cleanup the message
message.Text = b.replaceMention(message.Text)
message.Text = b.replaceVariable(message.Text)
message.Text = b.replaceChannel(message.Text)
message.Text = b.replaceURL(message.Text)
message.Text = html.UnescapeString(message.Text)
// Add the avatar
message.Avatar = b.users.getAvatar(message.UserID)
} }
// cleanup the message
message.Text = b.replaceMention(message.Text)
message.Text = b.replaceVariable(message.Text)
message.Text = b.replaceChannel(message.Text)
message.Text = b.replaceURL(message.Text)
message.Text = html.UnescapeString(message.Text)
// Add the avatar
message.Avatar = b.getAvatar(message.UserID)
b.Log.Debugf("<= Message is %#v", message) b.Log.Debugf("<= Message is %#v", message)
b.Remote <- *message b.Remote <- *message
} }
@@ -75,20 +75,17 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
// When we join a channel we update the full list of users as // 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 // well as the information for the channel that we joined as this
// should now tell that we are a member of it. // should now tell that we are a member of it.
b.channelsMutex.Lock() b.channels.registerChannel(ev.Channel)
b.channelsByID[ev.Channel.ID] = &ev.Channel
b.channelsByName[ev.Channel.Name] = &ev.Channel
b.channelsMutex.Unlock()
case *slack.ConnectedEvent: case *slack.ConnectedEvent:
b.si = ev.Info b.si = ev.Info
b.populateChannels(true) b.channels.populateChannels(true)
b.populateUsers(true) b.users.populateUsers(true)
case *slack.InvalidAuthEvent: case *slack.InvalidAuthEvent:
b.Log.Fatalf("Invalid Token %#v", ev) b.Log.Fatalf("Invalid Token %#v", ev)
case *slack.ConnectionErrorEvent: case *slack.ConnectionErrorEvent:
b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
case *slack.MemberJoinedChannelEvent: case *slack.MemberJoinedChannelEvent:
b.populateUser(ev.User) b.users.populateUser(ev.User)
case *slack.LatencyReport: case *slack.LatencyReport:
continue continue
default: default:
@@ -133,12 +130,18 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
return true return true
} }
// It seems ev.SubMessage.Edited == nil when slack unfurls. if ev.SubMessage != nil {
// Do not forward these messages. See Github issue #266. // It seems ev.SubMessage.Edited == nil when slack unfurls.
if ev.SubMessage != nil && // Do not forward these messages. See Github issue #266.
ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp && if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp &&
ev.SubMessage.Edited == nil { ev.SubMessage.Edited == nil {
return true return true
}
// see hidden subtypes at https://api.slack.com/events/message
// these messages are sent when we add a message to a thread #709
if ev.SubType == "message_replied" && ev.Hidden {
return true
}
} }
if len(ev.Files) > 0 { if len(ev.Files) > 0 {
@@ -210,7 +213,7 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
rmsg.Username = sSystemUser rmsg.Username = sSystemUser
rmsg.Event = config.EventJoinLeave rmsg.Event = config.EventJoinLeave
case sChannelTopic, sChannelPurpose: case sChannelTopic, sChannelPurpose:
b.populateChannels(false) b.channels.populateChannels(false)
rmsg.Event = config.EventTopicChange rmsg.Event = config.EventTopicChange
case sMessageChanged: case sMessageChanged:
rmsg.Text = ev.SubMessage.Text rmsg.Text = ev.SubMessage.Text
@@ -266,7 +269,7 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
} }
func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -316,36 +319,7 @@ func (b *Bslack) handleGetChannelMembers(rmsg *config.Message) bool {
return false return false
} }
cMembers := config.ChannelMembers{} cMembers := b.channels.getChannelMembers(b.users)
b.channelMembersMutex.RLock()
for channelID, members := range b.channelMembers {
for _, member := range members {
channelName := ""
userName := ""
userNick := ""
user := b.getUser(member)
if user != nil {
userName = user.Name
userNick = user.Profile.DisplayName
}
channel, _ := b.getChannelByID(channelID)
if channel != nil {
channelName = channel.Name
}
cMember := config.ChannelMember{
Username: userName,
Nick: userNick,
UserID: member,
ChannelID: channelID,
ChannelName: channelName,
}
cMembers = append(cMembers, cMember)
}
}
b.channelMembersMutex.RUnlock()
extra := make(map[string][]interface{}) extra := make(map[string][]interface{})
extra[config.EventGetChannelMembers] = append(extra[config.EventGetChannelMembers], cMembers) extra[config.EventGetChannelMembers] = append(extra[config.EventGetChannelMembers], cMembers)

View File

@@ -1,7 +1,6 @@
package bslack package bslack
import ( import (
"context"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
@@ -9,225 +8,14 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/nlopes/slack" "github.com/nlopes/slack"
"github.com/sirupsen/logrus"
) )
func (b *Bslack) 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 *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) {
return b.getChannelBy(name, b.channelsByName)
}
func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) {
return b.getChannelBy(ID, b.channelsByID)
}
func (b *Bslack) 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("%s: channel %s not found", b.Account, lookupKey)
}
const minimumRefreshInterval = 10 * time.Second
func (b *Bslack) populateUser(userID string) {
b.usersMutex.RLock()
_, exists := b.users[userID]
b.usersMutex.RUnlock()
if exists {
// already in cache
return
}
user, err := b.sc.GetUserInfo(userID)
if err != nil {
b.Log.Debugf("GetUserInfo failed for %v: %v", userID, err)
return
}
b.usersMutex.Lock()
b.users[userID] = user
b.usersMutex.Unlock()
}
func (b *Bslack) populateUsers(wait bool) {
b.refreshMutex.Lock()
if !wait && (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
}
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 = 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.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.earliestUserRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
func (b *Bslack) populateChannels(wait bool) {
b.refreshMutex.Lock()
if !wait && (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
}
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 = 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]
// 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.earliestChannelRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the // populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
// router before we apply message-dependent modifications. // router before we apply message-dependent modifications.
func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) { func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) {
// Use our own func because rtm.GetChannelInfo doesn't work for private channels. // 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 { if err != nil {
return nil, err return nil, err
} }
@@ -289,7 +77,7 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi
return nil return nil
} }
user := b.getUser(userID) user := b.users.getUser(userID)
if user == nil { if user == nil {
return fmt.Errorf("could not find information for user with id %s", ev.User) return fmt.Errorf("could not find information for user with id %s", ev.User)
} }
@@ -315,7 +103,7 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
break 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) b.Log.Errorf("Could not retrieve bot information: %#v", err)
return err return err
} }
@@ -360,7 +148,7 @@ func (b *Bslack) extractTopicOrPurpose(text string) (string, string) {
func (b *Bslack) replaceMention(text string) string { func (b *Bslack) replaceMention(text string) string {
replaceFunc := func(match string) string { replaceFunc := func(match string) string {
userID := strings.Trim(match, "@<>") userID := strings.Trim(match, "@<>")
if username := b.getUsername(userID); userID != "" { if username := b.users.getUsername(userID); userID != "" {
return "@" + username return "@" + username
} }
return match return match
@@ -404,16 +192,6 @@ func (b *Bslack) replaceCodeFence(text string) string {
return codeFenceRE.ReplaceAllString(text, "```") return codeFenceRE.ReplaceAllString(text, "```")
} }
func (b *Bslack) handleRateLimit(err error) error {
rateLimit, ok := err.(*slack.RateLimitedError)
if !ok {
return err
}
b.Log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
time.Sleep(rateLimit.RetryAfter)
return nil
}
// getUsersInConversation returns an array of userIDs that are members of channelID // getUsersInConversation returns an array of userIDs that are members of channelID
func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) { func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
channelMembers := []string{} channelMembers := []string{}
@@ -424,7 +202,7 @@ func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
members, nextCursor, err := b.sc.GetUsersInConversation(queryParams) members, nextCursor, err := b.sc.GetUsersInConversation(queryParams)
if err != nil { if err != nil {
if err = b.handleRateLimit(err); err != nil { if err = handleRateLimit(b.Log, err); err != nil {
return channelMembers, fmt.Errorf("Could not retrieve users in channels: %#v", err) return channelMembers, fmt.Errorf("Could not retrieve users in channels: %#v", err)
} }
continue continue
@@ -439,3 +217,13 @@ func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
} }
return channelMembers, nil return channelMembers, nil
} }
func handleRateLimit(log *logrus.Entry, err error) error {
rateLimit, ok := err.(*slack.RateLimitedError)
if !ok {
return err
}
log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
time.Sleep(rateLimit.RetryAfter)
return nil
}

View File

@@ -13,7 +13,9 @@ type BLegacy struct {
} }
func NewLegacy(cfg *bridge.Config) bridge.Bridger { func NewLegacy(cfg *bridge.Config) bridge.Bridger {
return &BLegacy{Bslack: newBridge(cfg)} b := &BLegacy{Bslack: newBridge(cfg)}
b.legacy = true
return b
} }
func (b *BLegacy) Connect() error { func (b *BLegacy) Connect() error {
@@ -55,14 +57,18 @@ func (b *BLegacy) Connect() error {
}) })
if b.GetString(tokenConfig) != "" { if b.GetString(tokenConfig) != "" {
b.Log.Info("Connecting using token (receiving)") 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() b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection() go b.rtm.ManageConnection()
go b.handleSlack() go b.handleSlack()
} }
} else if b.GetString(tokenConfig) != "" { } else if b.GetString(tokenConfig) != "" {
b.Log.Info("Connecting using token (sending and receiving)") 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() b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection() go b.rtm.ManageConnection()
go b.handleSlack() go b.handleSlack()

View File

@@ -30,20 +30,9 @@ type Bslack struct {
uuid string uuid string
useChannelID bool useChannelID bool
users map[string]*slack.User channels *channels
usersMutex sync.RWMutex users *users
legacy bool
channelsByID map[string]*slack.Channel
channelsByName map[string]*slack.Channel
channelsMutex sync.RWMutex
channelMembers map[string][]string
channelMembersMutex sync.RWMutex
refreshInProgress bool
earliestChannelRefresh time.Time
earliestUserRefresh time.Time
refreshMutex sync.Mutex
} }
const ( const (
@@ -94,14 +83,9 @@ func newBridge(cfg *bridge.Config) *Bslack {
cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err) cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err)
} }
b := &Bslack{ b := &Bslack{
Config: cfg, Config: cfg,
uuid: xid.New().String(), uuid: xid.New().String(),
cache: newCache, cache: newCache,
users: map[string]*slack.User{},
channelsByID: map[string]*slack.Channel{},
channelsByName: map[string]*slack.Channel{},
earliestChannelRefresh: time.Now(),
earliestUserRefresh: time.Now(),
} }
return b return b
} }
@@ -121,7 +105,12 @@ func (b *Bslack) Connect() error {
// If we have a token we use the Slack websocket-based RTM for both sending and receiving. // If we have a token we use the Slack websocket-based RTM for both sending and receiving.
if token := b.GetString(tokenConfig); token != "" { if token := b.GetString(tokenConfig); token != "" {
b.Log.Info("Connecting using token") b.Log.Info("Connecting using token")
b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug"))) 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() b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection() go b.rtm.ManageConnection()
go b.handleSlack() go b.handleSlack()
@@ -163,9 +152,21 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
return nil return nil
} }
b.populateChannels(false) // try to join a channel when in legacy
if b.legacy {
_, err := b.sc.JoinChannel(channel.Name)
if err != nil {
switch err.Error() {
case "name_taken", "restricted_action":
case "default":
return err
}
}
}
channelInfo, err := b.getChannel(channel.Name) b.channels.populateChannels(false)
channelInfo, err := b.channels.getChannel(channel.Name)
if err != nil { if err != nil {
return fmt.Errorf("could not join channel: %#v", err) return fmt.Errorf("could not join channel: %#v", err)
} }
@@ -175,7 +176,8 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
channel.Name = channelInfo.Name channel.Name = channelInfo.Name
} }
if !channelInfo.IsMember { // we can't join a channel unless we are using legacy tokens #651
if !channelInfo.IsMember && !b.legacy {
return fmt.Errorf("slack integration that matterbridge is using is not member of channel '%s', please add it manually", channelInfo.Name) return fmt.Errorf("slack integration that matterbridge is using is not member of channel '%s', please add it manually", channelInfo.Name)
} }
return nil return nil
@@ -275,7 +277,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
return "", nil return "", nil
} }
channelInfo, err := b.getChannel(msg.Channel) channelInfo, err := b.channels.getChannel(msg.Channel)
if err != nil { if err != nil {
return "", fmt.Errorf("could not send message: %v", err) return "", fmt.Errorf("could not send message: %v", err)
} }
@@ -351,7 +353,7 @@ func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Ch
if err == nil { if err == nil {
return nil return nil
} }
if err = b.handleRateLimit(err); err != nil { if err = handleRateLimit(b.Log, err); err != nil {
return err return err
} }
} }
@@ -392,7 +394,7 @@ func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel)
return true, nil 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) b.Log.Errorf("Failed to delete user message from Slack: %#v", err)
return true, err return true, err
} }
@@ -411,7 +413,7 @@ func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (b
return true, 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) b.Log.Errorf("Failed to edit user message on Slack: %#v", err)
return true, err return true, err
} }
@@ -424,14 +426,18 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
return "", nil return "", nil
} }
messageOptions := b.prepareMessageOptions(msg) messageOptions := b.prepareMessageOptions(msg)
messageOptions = append(messageOptions, slack.MsgOptionText(msg.Text, false)) messageOptions = append(
messageOptions,
slack.MsgOptionText(msg.Text, false),
slack.MsgOptionEnableLinkUnfurl(),
)
for { for {
_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...) _, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
if err == nil { if err == nil {
return id, 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) b.Log.Errorf("Failed to sent user message to Slack: %#v", err)
return "", err return "", err
} }

View File

@@ -0,0 +1,336 @@
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{})
defer func() {
// Wake up any waiting goroutines and remove the synchronization point.
close(b.usersSyncPoints[userID])
delete(b.usersSyncPoints, userID)
}()
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
}
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

@@ -5,10 +5,11 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"unicode/utf16"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/go-telegram-bot-api/telegram-bot-api" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
) )
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message { func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
@@ -125,6 +126,11 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// handle groups // handle groups
message = b.handleGroups(&rmsg, message, update) message = b.handleGroups(&rmsg, message, update)
if message == nil {
b.Log.Error("message is nil, this shouldn't happen.")
continue
}
// set the ID's from the channel or group message // set the ID's from the channel or group message
rmsg.ID = strconv.Itoa(message.MessageID) rmsg.ID = strconv.Itoa(message.MessageID)
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10) rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
@@ -144,6 +150,9 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// quote the previous message // quote the previous message
b.handleQuoting(&rmsg, message) b.handleQuoting(&rmsg, message)
// handle entities (adding URLs)
b.handleEntities(&rmsg, message)
if rmsg.Text != "" || len(rmsg.Extra) > 0 { if rmsg.Text != "" || len(rmsg.Extra) > 0 {
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text) rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
// channels don't have (always?) user information. see #410 // channels don't have (always?) user information. see #410
@@ -245,6 +254,15 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
if err != nil { if err != nil {
return err 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) helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
return nil return nil
} }
@@ -344,3 +362,27 @@ func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string
format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1) format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)
return format 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
}
utfEncodedString := utf16.Encode([]rune(rmsg.Text))
if e.Offset+e.Length > len(utfEncodedString) {
b.Log.Errorf("entity length is too long %d > %d", e.Offset+e.Length, len(utfEncodedString))
continue
}
link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length])
rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1)
}
}
}

View File

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

View File

@@ -5,10 +5,7 @@ import (
"time" "time"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
whatsappExt "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
) )
/* /*
@@ -21,6 +18,10 @@ Check:
// HandleError received from WhatsApp // HandleError received from WhatsApp
func (b *Bwhatsapp) HandleError(err error) { func (b *Bwhatsapp) HandleError(err error) {
// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
if strings.Contains(err.Error(), "error processing data: received invalid data") {
return
}
b.Log.Errorf("%v", err) // TODO implement proper handling? at least respond to different error types b.Log.Errorf("%v", err) // TODO implement proper handling? at least respond to different error types
} }
@@ -57,7 +58,7 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
// mentions comes as telephone numbers and we don't want to expose it to other bridges // mentions comes as telephone numbers and we don't want to expose it to other bridges
// replace it with something more meaninful to others // replace it with something more meaninful to others
mention := b.getSenderNotify(numberAndSuffix[0] + whatsappExt.NewUserSuffix) mention := b.getSenderNotify(numberAndSuffix[0] + "@s.whatsapp.net")
if mention == "" { if mention == "" {
mention = "someone" mention = "someone"
} }

View File

@@ -2,13 +2,22 @@ package bwhatsapp
import ( import (
"encoding/gob" "encoding/gob"
"encoding/json"
"errors" "errors"
"fmt"
"os" "os"
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go" qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
) )
type ProfilePicInfo struct {
URL string `json:"eurl"`
Tag string `json:"tag"`
Status int16 `json:"status"`
}
func qrFromTerminal(invert bool) chan string { func qrFromTerminal(invert bool) chan string {
qr := make(chan string) qr := make(chan string)
go func() { go func() {
@@ -82,3 +91,17 @@ func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
} }
return "" return ""
} }
func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
data, err := b.conn.GetProfilePicThumb(jid)
if err != nil {
return nil, fmt.Errorf("failed to get avatar: %v", err)
}
content := <-data
info := &ProfilePicInfo{}
err = json.Unmarshal([]byte(content), info)
if err != nil {
return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
}
return info, nil
}

View File

@@ -11,10 +11,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
whatsappExt "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
) )
const ( const (
@@ -29,10 +26,8 @@ type Bwhatsapp struct {
*bridge.Config *bridge.Config
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21 // https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21
session *whatsapp.Session session *whatsapp.Session
conn *whatsapp.Conn conn *whatsapp.Conn
// https://github.com/tulir/mautrix-whatsapp/blob/master/whatsapp-ext/whatsapp.go
connExt *whatsappExt.ExtendedConn
startedAt uint64 startedAt uint64
users map[string]whatsapp.Contact users map[string]whatsapp.Contact
@@ -74,8 +69,6 @@ func (b *Bwhatsapp) Connect() error {
} }
b.conn = conn 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.conn.AddHandler(b)
b.Log.Debugln("WhatsApp connection successful") b.Log.Debugln("WhatsApp connection successful")
@@ -89,7 +82,7 @@ func (b *Bwhatsapp) Connect() error {
b.Log.Debugln("Restoring WhatsApp session..") b.Log.Debugln("Restoring WhatsApp session..")
// https://github.com/Rhymen/go-whatsapp#restore // https://github.com/Rhymen/go-whatsapp#restore
session, err = b.conn.RestoreSession(session) session, err = b.conn.RestoreWithSession(session)
if err != nil { if err != nil {
// TODO return or continue to normal login? // TODO return or continue to normal login?
// restore session connection timed out (I couldn't get over it without logging in again) // restore session connection timed out (I couldn't get over it without logging in again)
@@ -130,7 +123,7 @@ func (b *Bwhatsapp) Connect() error {
b.Log.Debug("Getting user avatars..") b.Log.Debug("Getting user avatars..")
for jid := range b.users { for jid := range b.users {
info, err := b.connExt.GetProfilePicThumb(jid) info, err := b.GetProfilePicThumb(jid)
if err != nil { if err != nil {
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err) b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
@@ -294,7 +287,7 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
} }
text.Info.Id = strings.ToUpper(hex.EncodeToString(bytes)) text.Info.Id = strings.ToUpper(hex.EncodeToString(bytes))
err := b.conn.Send(text) _, err := b.conn.Send(text)
return text.Info.Id, err return text.Info.Id, err
} }

View File

@@ -2,7 +2,9 @@ package bxmpp
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"strings" "strings"
"sync"
"time" "time"
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
@@ -14,50 +16,31 @@ import (
) )
type Bxmpp struct { type Bxmpp struct {
xc *xmpp.Client
xmppMap map[string]string
*bridge.Config *bridge.Config
startTime time.Time startTime time.Time
xc *xmpp.Client
xmppMap map[string]string
connected bool
sync.RWMutex
} }
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
b := &Bxmpp{Config: cfg} return &Bxmpp{
b.xmppMap = make(map[string]string) Config: cfg,
return b xmppMap: make(map[string]string),
}
} }
func (b *Bxmpp) Connect() error { func (b *Bxmpp) Connect() error {
var err error
b.Log.Infof("Connecting %s", b.GetString("Server")) b.Log.Infof("Connecting %s", b.GetString("Server"))
b.xc, err = b.createXMPP() if err := b.createXMPP(); err != nil {
if err != nil {
b.Log.Debugf("%#v", err) b.Log.Debugf("%#v", err)
return err return err
} }
b.Log.Info("Connection succeeded") b.Log.Info("Connection succeeded")
go func() { go b.manageConnection()
initial := true
bf := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
for {
if initial {
b.handleXMPP()
initial = false
}
d := bf.Duration()
b.Log.Infof("Disconnected. Reconnecting in %s", d)
time.Sleep(d)
b.xc, err = b.createXMPP()
if err == nil {
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels}
b.handleXMPP()
bf.Reset()
}
}
}()
return nil return nil
} }
@@ -76,40 +59,58 @@ func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error {
} }
func (b *Bxmpp) Send(msg config.Message) (string, error) { func (b *Bxmpp) Send(msg config.Message) (string, error) {
// should be fixed by using a cache instead of dropping
if !b.Connected() {
return "", fmt.Errorf("bridge %s not connected, dropping message %#v to bridge", b.Account, msg)
}
// ignore delete messages // ignore delete messages
if msg.Event == config.EventMsgDelete { if msg.Event == config.EventMsgDelete {
return "", nil return "", nil
} }
b.Log.Debugf("=> Receiving %#v", msg) b.Log.Debugf("=> Receiving %#v", msg)
// Upload a file (in xmpp case send the upload URL because xmpp has no native upload support) // Upload a file (in XMPP case send the upload URL because XMPP has no native upload support).
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: rmsg.Channel + "@" + b.GetString("Muc"), Text: rmsg.Username + rmsg.Text}) b.Log.Debugf("=> Sending attachement message %#v", rmsg)
if _, err := b.xc.Send(xmpp.Chat{
Type: "groupchat",
Remote: rmsg.Channel + "@" + b.GetString("Muc"),
Text: rmsg.Username + rmsg.Text,
}); err != nil {
b.Log.WithError(err).Error("Unable to send message with share URL.")
}
} }
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg) return "", b.handleUploadFile(&msg)
} }
} }
var msgreplaceid string var msgReplaceID string
msgid := xid.New().String() msgID := xid.New().String()
if msg.ID != "" { if msg.ID != "" {
msgid = msg.ID msgID = msg.ID
msgreplaceid = msg.ID msgReplaceID = msg.ID
} }
// Post normal message // Post normal message.
_, err := b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text, ID: msgid, ReplaceID: msgreplaceid}) b.Log.Debugf("=> Sending message %#v", msg)
if err != nil { if _, err := b.xc.Send(xmpp.Chat{
Type: "groupchat",
Remote: msg.Channel + "@" + b.GetString("Muc"),
Text: msg.Username + msg.Text,
ID: msgID,
ReplaceID: msgReplaceID,
}); err != nil {
return "", err return "", err
} }
return msgid, nil return msgID, nil
} }
func (b *Bxmpp) createXMPP() (*xmpp.Client, error) { func (b *Bxmpp) createXMPP() error {
tc := new(tls.Config) tc := &tls.Config{
tc.InsecureSkipVerify = b.GetBool("SkipTLSVerify") ServerName: strings.Split(b.GetString("Jid"), "@")[1],
tc.ServerName = strings.Split(b.GetString("Server"), ":")[0] InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec
}
options := xmpp.Options{ options := xmpp.Options{
Host: b.GetString("Server"), Host: b.GetString("Server"),
User: b.GetString("Jid"), User: b.GetString("Jid"),
@@ -127,7 +128,54 @@ func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
} }
var err error var err error
b.xc, err = options.NewClient() b.xc, err = options.NewClient()
return b.xc, err return err
}
func (b *Bxmpp) manageConnection() {
b.setConnected(true)
initial := true
bf := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
// Main connection loop. Each iteration corresponds to a successful
// connection attempt and the subsequent handling of the connection.
for {
if initial {
initial = false
} else {
b.Remote <- config.Message{
Username: "system",
Text: "rejoin",
Channel: "",
Account: b.Account,
Event: config.EventRejoinChannels,
}
}
if err := b.handleXMPP(); err != nil {
b.Log.WithError(err).Error("Disconnected.")
b.setConnected(false)
}
// Reconnection loop using an exponential back-off strategy. We
// only break out of the loop if we have successfully reconnected.
for {
d := bf.Duration()
b.Log.Infof("Reconnecting in %s.", d)
time.Sleep(d)
b.Log.Infof("Reconnecting now.")
if err := b.createXMPP(); err == nil {
b.setConnected(true)
bf.Reset()
break
}
b.Log.Warn("Failed to reconnect.")
}
}
} }
func (b *Bxmpp) xmppKeepAlive() chan bool { func (b *Bxmpp) xmppKeepAlive() chan bool {
@@ -139,8 +187,7 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
select { select {
case <-ticker.C: case <-ticker.C:
b.Log.Debugf("PING") b.Log.Debugf("PING")
err := b.xc.PingC2S("", "") if err := b.xc.PingC2S("", ""); err != nil {
if err != nil {
b.Log.Debugf("PING failed %#v", err) b.Log.Debugf("PING failed %#v", err)
} }
case <-done: case <-done:
@@ -152,31 +199,35 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
} }
func (b *Bxmpp) handleXMPP() error { func (b *Bxmpp) handleXMPP() error {
var ok bool
var msgid string
b.startTime = time.Now() b.startTime = time.Now()
done := b.xmppKeepAlive() done := b.xmppKeepAlive()
defer close(done) defer close(done)
for { for {
m, err := b.xc.Recv() m, err := b.xc.Recv()
if err != nil { if err != nil {
return err return err
} }
switch v := m.(type) { switch v := m.(type) {
case xmpp.Chat: case xmpp.Chat:
if v.Type == "groupchat" { if v.Type == "groupchat" {
b.Log.Debugf("== Receiving %#v", v) b.Log.Debugf("== Receiving %#v", v)
event := ""
// skip invalid messages // Skip invalid messages.
if b.skipMessage(v) { if b.skipMessage(v) {
continue continue
} }
var event string
if strings.Contains(v.Text, "has set the subject to:") { if strings.Contains(v.Text, "has set the subject to:") {
event = config.EventTopicChange event = config.EventTopicChange
} }
msgid = v.ID
msgID := v.ID
if v.ReplaceID != "" { if v.ReplaceID != "" {
msgid = v.ReplaceID msgID = v.ReplaceID
} }
rmsg := config.Message{ rmsg := config.Message{
Username: b.parseNick(v.Remote), Username: b.parseNick(v.Remote),
@@ -184,21 +235,23 @@ func (b *Bxmpp) handleXMPP() error {
Channel: b.parseChannel(v.Remote), Channel: b.parseChannel(v.Remote),
Account: b.Account, Account: b.Account,
UserID: v.Remote, UserID: v.Remote,
ID: msgid, ID: msgID,
Event: event, Event: event,
} }
// check if we have an action event // Check if we have an action event.
var ok bool
rmsg.Text, ok = b.replaceAction(rmsg.Text) rmsg.Text, ok = b.replaceAction(rmsg.Text)
if ok { if ok {
rmsg.Event = config.EventUserAction rmsg.Event = config.EventUserAction
} }
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg) b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
} }
case xmpp.Presence: case xmpp.Presence:
// do nothing // Do nothing.
} }
} }
} }
@@ -211,30 +264,41 @@ func (b *Bxmpp) replaceAction(text string) (string, bool) {
} }
// handleUploadFile handles native upload of files // handleUploadFile handles native upload of files
func (b *Bxmpp) handleUploadFile(msg *config.Message) (string, error) { func (b *Bxmpp) handleUploadFile(msg *config.Message) error {
var urldesc = "" var urlDesc string
for _, f := range msg.Extra["file"] { for _, file := range msg.Extra["file"] {
fi := f.(config.FileInfo) fileInfo := file.(config.FileInfo)
if fi.Comment != "" { if fileInfo.Comment != "" {
msg.Text += fi.Comment + ": " msg.Text += fileInfo.Comment + ": "
} }
if fi.URL != "" { if fileInfo.URL != "" {
msg.Text = fi.URL msg.Text = fileInfo.URL
if fi.Comment != "" { if fileInfo.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL msg.Text = fileInfo.Comment + ": " + fileInfo.URL
urldesc = fi.Comment urlDesc = fileInfo.Comment
} }
} }
_, err := b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text}) if _, err := b.xc.Send(xmpp.Chat{
if err != nil { Type: "groupchat",
return "", err Remote: msg.Channel + "@" + b.GetString("Muc"),
Text: msg.Username + msg.Text,
}); err != nil {
return err
} }
if fi.URL != "" {
b.xc.SendOOB(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Ooburl: fi.URL, Oobdesc: urldesc}) if fileInfo.URL != "" {
if _, err := b.xc.SendOOB(xmpp.Chat{
Type: "groupchat",
Remote: msg.Channel + "@" + b.GetString("Muc"),
Ooburl: fileInfo.URL,
Oobdesc: urlDesc,
}); err != nil {
b.Log.WithError(err).Warn("Failed to send share URL.")
}
} }
} }
return "", nil return nil
} }
func (b *Bxmpp) parseNick(remote string) string { func (b *Bxmpp) parseNick(remote string) string {
@@ -279,6 +343,17 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
} }
// skip delayed messages // skip delayed messages
t := time.Time{} return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5
return message.Stamp != t }
func (b *Bxmpp) setConnected(state bool) {
b.Lock()
b.connected = state
defer b.Unlock()
}
func (b *Bxmpp) Connected() bool {
b.RLock()
defer b.RUnlock()
return b.connected
} }

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@@ -18,12 +19,11 @@ type Bzulip struct {
bot *gzb.Bot bot *gzb.Bot
streams map[int]string streams map[int]string
*bridge.Config *bridge.Config
channelToTopic map[string]string
sync.RWMutex sync.RWMutex
} }
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
return &Bzulip{Config: cfg, streams: make(map[int]string), channelToTopic: make(map[string]string)} return &Bzulip{Config: cfg, streams: make(map[int]string)}
} }
func (b *Bzulip) Connect() error { func (b *Bzulip) Connect() error {
@@ -48,9 +48,6 @@ func (b *Bzulip) Disconnect() error {
} }
func (b *Bzulip) JoinChannel(channel config.ChannelInfo) error { func (b *Bzulip) JoinChannel(channel config.ChannelInfo) error {
b.Lock()
defer b.Unlock()
b.channelToTopic[channel.Name] = channel.Options.Topic
return nil return nil
} }
@@ -116,11 +113,13 @@ func (b *Bzulip) handleQueue() error {
case gzb.BadEventQueueError: case gzb.BadEventQueueError:
b.Log.Info("got a bad event queue id error, reconnecting") b.Log.Info("got a bad event queue id error, reconnecting")
b.bot.Queues = nil b.bot.Queues = nil
b.q, err = b.bot.RegisterAll() for {
if err != nil { b.q, err = b.bot.RegisterAll()
b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err) if err != nil {
time.Sleep(time.Second * 10) b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
continue time.Sleep(time.Second * 10)
}
break
} }
case gzb.HeartbeatError: case gzb.HeartbeatError:
b.Log.Debug("heartbeat received.") b.Log.Debug("heartbeat received.")
@@ -136,7 +135,14 @@ func (b *Bzulip) handleQueue() error {
if m.SenderEmail == b.GetString("login") { if m.SenderEmail == b.GetString("login") {
continue 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("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg) b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
@@ -147,12 +153,11 @@ func (b *Bzulip) handleQueue() error {
} }
func (b *Bzulip) sendMessage(msg config.Message) (string, error) { func (b *Bzulip) sendMessage(msg config.Message) (string, error) {
topic := "matterbridge" topic := ""
if b.GetString("topic") != "" { if strings.Contains(msg.Channel, "/topic:") {
topic = b.GetString("topic") res := strings.Split(msg.Channel, "/topic:")
} topic = res[1]
if res := b.getTopic(msg.Channel); res != "" { msg.Channel = res[0]
topic = res
} }
m := gzb.Message{ m := gzb.Message{
Stream: msg.Channel, Stream: msg.Channel,
@@ -200,9 +205,3 @@ func (b *Bzulip) handleUploadFile(msg *config.Message) (string, error) {
} }
return "", nil return "", nil
} }
func (b *Bzulip) getTopic(channel string) string {
b.RLock()
defer b.RUnlock()
return b.channelToTopic[channel]
}

View File

@@ -1,4 +1,81 @@
# v1.14.0-rc1 # dev
# v1.15.1
## Enhancements
* discord: Support bulk deletions #851
* discord: Support channels in categories #863 (use category/channel. See matterbridge.toml.sample for more info)
* mattermost: Add an option to skip the Mattermost server version check #849
## Bugfix
* xmpp: fix segfault when disconnected/reconnected #856
* telegram: fix panic in handleEntities #858
# v1.15.0
## New features
* Add scripting (tengo) support for every outgoing message (#806)
See https://github.com/42wim/matterbridge/wiki/Settings#tengo and
https://github.com/42wim/matterbridge/wiki/Settings#outmessage for more information
* Add tengo support to RemoteNickFormat (#793)
See https://github.com/42wim/matterbridge/wiki/Settings#remotenickformat-2
* Deprecated `Message` under `[tengo]` to `InMessage`
## Enhancements
* general: Forward only user-typing messages if supported by protocol (#832)
* general: updated wiki with all possible settings: https://github.com/42wim/matterbridge/wiki/Settings
* tengo: Add msg event to tengo
* xmpp: Verify TLS against JID domain, not the host. (xmpp) (#834)
* xmpp: Allow messages with timestamp (xmpp). Fixes #835 (#847)
* irc: Add verbose IRC joins/parts (ident@host) (#805)
See https://github.com/42wim/matterbridge/wiki/Settings#verbosejoinpart
* rocketchat: Add useraction support (rocketchat). Closes #772 (#794)
## Bugfix
* slack: Fix regression in autojoining with legacy tokens (slack). Fixes #651 (#848)
* xmpp: Revert xmpp to orig behaviour. Closes #844
* whatsapp: Update github.com/Rhymen/go-whatsapp vendor. Fixes #843
* mattermost: Update channels of all teams (mattermost)
This release couldn't exist without the following contributors:
@42wim, @Helcaraxan, @chotaire, @qaisjp, @dajohi, @kousu
# v1.14.4
## Bugfix
* mattermost: Add Id to EditMessage (mattermost). Fixes #802
* mattermost: Fix panic on nil message.Post (mattermost). Fixes #804
* mattermost: Handle unthreaded messages (mattermost). Fixes #803
* mattermost: Use paging in initUser and UpdateUsers (mattermost)
* slack: Add lacking clean-up in Slack synchronisation (#811)
* slack: Disable user lookups on delete messages (slack) (#812)
# v1.14.3
## Bugfix
* irc: Fix deadlock on reconnect (irc). Closes #757
# v1.14.2
## Bugfix
* general: Update tengo vendor and load the stdlib. Fixes #789 (#792)
* rocketchat: Look up #channel too (rocketchat). Fix #773 (#775)
* slack: Ignore messagereplied and hidden messages (slack). Fixes #709 (#779)
* telegram: Handle nil message (telegram). Fixes #777
* irc: Use default nick if none specified (irc). Fixes #785
* irc: Return when not connected and drop a message (irc). Fixes #786
* irc: Revert fix for #722 (Support quits from irc correctly). Closes #781
## Contributors
This release couldn't exist without the following contributors:
@42wim, @Helcaraxan, @dajohi
# 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 ## New features
* whatsapp: new protocol added. Add initial WhatsApp support (#711) Thanks to @KrzysztofMadejski * whatsapp: new protocol added. Add initial WhatsApp support (#711) Thanks to @KrzysztofMadejski
@@ -10,18 +87,29 @@
* rocketchat: add support for the rocketchat API. Sending to rocketchat now supports uploading of files, editing and deleting of messages. * 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: Support join/leaves from discord. Closes #654 (#721)
* discord: Allow sending discriminator with Discord username (#726). See `UseDiscriminator` in matterbridge.toml.sample * discord: Allow sending discriminator with Discord username (#726). See `UseDiscriminator` in matterbridge.toml.sample
* zulip: Allow zulip bridge to specify topic per channel. Closes #701 (#723). See `Topic` in matterbridge.toml.sample
* slack: Add extra debug option (slack). See `Debug` in the slack section 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 ## 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: 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: add support for (older) unrealircd versions. #708
* irc: Support quits from irc correctly. Fixes #722 (#724) * irc: Support quits from irc correctly. Fixes #722 (#724)
* matrix: Send username when uploading video/images (matrix). Fixes #715 (#717) * matrix: Send username when uploading video/images (matrix). Fixes #715 (#717)
* matrix: Send notices on join/parts (matrix). Fixes #712 (#716) * matrix: Trim <p> and </p> tags (matrix). Closes #686 (#753)
* matrix: Detect html nicks in RemoteNickFormat (matrix). Fixes #696 (#719)
* slack: Hint at thread replies when messages are unthreaded (slack) (#684) * 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) * 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 ## Contributors
This release couldn't exist without the following contributors: This release couldn't exist without the following contributors:

View File

@@ -1,5 +1,8 @@
#!/bin/bash #!/usr/bin/env bash
go version | grep go1.11 || exit set -u -e -x -o pipefail
go version | grep go1.12 || exit
VERSION=$(git describe --tags) VERSION=$(git describe --tags)
mkdir ci/binaries mkdir ci/binaries
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe

17
ci/lint.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -u -e -x -o pipefail
if [[ -n "${GOLANGCI_VERSION-}" ]]; then
# 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}
fi
# Run the linter.
golangci-lint run
if [[ "${GO111MODULE-off}" == "on" ]]; then
# If Go modules are active then check that dependencies are correctly maintained.
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)
fi

17
ci/test.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -u -e -x -o pipefail
if [[ -n "${REPORT_COVERAGE+cover}" ]]; then
# 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
fi
# Run all the tests with the race detector and generate coverage.
go test -v -race -coverprofile c.out ./...
if [[ -n "${REPORT_COVERAGE+cover}" && "${TRAVIS_SECURE_ENV_VARS}" == "true" ]]; then
# Upload test coverage to CodeClimate.
./cc-test-reporter after-build
fi

View File

@@ -0,0 +1,7 @@
// See https://github.com/42wim/matterbridge/issues/798
// if we're not sending to an irc bridge we strip the IRC colors
if outProtocol != "irc" {
re := text.re_compile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
msgText=re.replace(msgText,"")
}

View File

@@ -0,0 +1,16 @@
/*
This script will return the nick except with multi-character usernames
containing a zero-width space between the first and second character letter.
Single character usernames will be left untouched.
This is useful to prevent remote users from nickalerting
IRC users of the same name when the remote user speaks.
This result can be used in {TENGO} in RemoteNickFormat.
*/
result = nick
if len(nick) > 1 {
result = string(nick[0]) + "" + nick[1:]
}

View File

@@ -0,0 +1,9 @@
/*
This script will return the current time in kitchen format if the protocol (of the remote bridge) isn't irc
See https://github.com/d5/tengo/blob/master/docs/stdlib-times.md
This result can be used in {TENGO} in RemoteNickFormat
*/
times := import("times")
if protocol != "irc" {
result=times.time_format(times.now(),times.format_kitchen)
}

View File

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

View File

@@ -9,7 +9,9 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/internal"
"github.com/d5/tengo/script" "github.com/d5/tengo/script"
"github.com/d5/tengo/stdlib"
lru "github.com/hashicorp/golang-lru" lru "github.com/hashicorp/golang-lru"
"github.com/peterhellberg/emojilib" "github.com/peterhellberg/emojilib"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@@ -92,6 +94,9 @@ func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
Bridge: br, Bridge: br,
} }
// add the actual bridger for this protocol to this bridge using the bridgeMap // add the actual bridger for this protocol to this bridge using the bridgeMap
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) br.Bridger = gw.Router.BridgeMap[br.Protocol](brconfig)
} }
gw.mapChannelsToBridge(br) gw.mapChannelsToBridge(br)
@@ -156,6 +161,10 @@ func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) {
gw.logger.Errorf("Mattermost channels do not start with a #: remove the # in %s", br.Channel) gw.logger.Errorf("Mattermost channels do not start with a #: remove the # in %s", br.Channel)
os.Exit(1) 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 ID := br.Channel + br.Account
if _, ok := gw.Channels[ID]; !ok { if _, ok := gw.Channels[ID]; !ok {
channel := &config.ChannelInfo{ channel := &config.ChannelInfo{
@@ -204,23 +213,6 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
return channels 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 // if source channel is in only, do nothing
for _, channel := range gw.Channels { for _, channel := range gw.Channels {
// lookup the channel from the message // lookup the channel from the message
@@ -340,6 +332,11 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri
nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1) nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1)
nick = strings.Replace(nick, "{NICK}", msg.Username, -1) nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1) nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1)
tengoNick, err := gw.modifyUsernameTengo(msg, br)
if err != nil {
gw.logger.Errorf("modifyUsernameTengo error: %s", err)
}
nick = strings.Replace(nick, "{TENGO}", tengoNick, -1) //nolint:gocritic
return nick return nick
} }
@@ -356,6 +353,9 @@ func (gw *Gateway) modifyMessage(msg *config.Message) {
if err := modifyMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil { if err := modifyMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil {
gw.logger.Errorf("TengoModifyMessage failed: %s", err) gw.logger.Errorf("TengoModifyMessage failed: %s", err)
} }
if err := modifyMessageTengo(gw.BridgeValues().Tengo.Message, msg); err != nil {
gw.logger.Errorf("Tengo.Message failed: %s", err)
}
// replace :emoji: to unicode // replace :emoji: to unicode
msg.Text = emojilib.Replace(msg.Text) msg.Text = emojilib.Replace(msg.Text)
@@ -430,6 +430,11 @@ func (gw *Gateway) SendMessage(
msg.ParentID = "msg-parent-not-found" msg.ParentID = "msg-parent-not-found"
} }
err := gw.modifySendMessageTengo(rmsg, &msg, dest)
if err != nil {
gw.logger.Errorf("modifySendMessageTengo: %s", err)
}
// if we are using mattermost plugin account, send messages to MattermostPlugin channel // if we are using mattermost plugin account, send messages to MattermostPlugin channel
// that can be picked up by the mattermost matterbridge plugin // that can be picked up by the mattermost matterbridge plugin
if dest.Account == "mattermost.plugin" { if dest.Account == "mattermost.plugin" {
@@ -496,6 +501,7 @@ func modifyMessageTengo(filename string, msg *config.Message) error {
return err return err
} }
s := script.New(res) s := script.New(res)
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("msgText", msg.Text) _ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username) _ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgAccount", msg.Account) _ = s.Add("msgAccount", msg.Account)
@@ -511,3 +517,77 @@ func modifyMessageTengo(filename string, msg *config.Message) error {
msg.Username = c.Get("msgUsername").String() msg.Username = c.Get("msgUsername").String()
return nil return nil
} }
func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) (string, error) {
filename := gw.BridgeValues().Tengo.RemoteNickFormat
if filename == "" {
return "", nil
}
res, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
s := script.New(res)
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("result", "")
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("nick", msg.Username)
_ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel)
_ = s.Add("channel", msg.Channel)
_ = s.Add("msgProtocol", msg.Protocol)
_ = s.Add("remoteAccount", br.Account)
_ = s.Add("protocol", br.Protocol)
_ = s.Add("bridge", br.Name)
_ = s.Add("gateway", gw.Name)
c, err := s.Compile()
if err != nil {
return "", err
}
if err := c.Run(); err != nil {
return "", err
}
return c.Get("result").String(), nil
}
func (gw *Gateway) modifySendMessageTengo(origmsg *config.Message, msg *config.Message, br *bridge.Bridge) error {
filename := gw.BridgeValues().Tengo.OutMessage
var res []byte
var err error
if filename == "" {
res, err = internal.Asset("tengo/outmessage.tengo")
if err != nil {
return err
}
} else {
res, err = ioutil.ReadFile(filename)
if err != nil {
return err
}
}
s := script.New(res)
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("inAccount", origmsg.Account)
_ = s.Add("inProtocol", origmsg.Protocol)
_ = s.Add("inChannel", origmsg.Channel)
_ = s.Add("inGateway", origmsg.Gateway)
_ = s.Add("inEvent", origmsg.Event)
_ = s.Add("outAccount", br.Account)
_ = s.Add("outProtocol", br.Protocol)
_ = s.Add("outChannel", msg.Channel)
_ = s.Add("outGateway", gw.Name)
_ = s.Add("outEvent", msg.Event)
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
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

@@ -14,6 +14,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway/bridgemap"
) )
// handleEventFailure handles failures and reconnects bridges. // handleEventFailure handles failures and reconnects bridges.
@@ -190,6 +191,14 @@ func (gw *Gateway) ignoreEvent(event string, dest *bridge.Bridge) bool {
func (gw *Gateway) handleMessage(rmsg *config.Message, dest *bridge.Bridge) []*BrMsgID { func (gw *Gateway) handleMessage(rmsg *config.Message, dest *bridge.Bridge) []*BrMsgID {
var brMsgIDs []*BrMsgID var brMsgIDs []*BrMsgID
// Not all bridges support "user is typing" indications so skip the message
// if the targeted bridge does not support it.
if rmsg.Event == config.EventUserTyping {
if _, ok := bridgemap.UserTypingSupport[dest.Protocol]; !ok {
return nil
}
}
// if we have an attached file, or other info // if we have an attached file, or other info
if rmsg.Extra != nil && len(rmsg.Extra[config.EventFileFailureSize]) != 0 && rmsg.Text == "" { if rmsg.Extra != nil && len(rmsg.Extra[config.EventFileFailureSize]) != 0 && rmsg.Text == "" {
return brMsgIDs return brMsgIDs

View File

@@ -125,6 +125,8 @@ func (r *Router) handleReceive() {
r.handleEventGetChannelMembers(&msg) r.handleEventGetChannelMembers(&msg)
r.handleEventFailure(&msg) r.handleEventFailure(&msg)
r.handleEventRejoinChannels(&msg) r.handleEventRejoinChannels(&msg)
filesHandled := false
for _, gw := range r.Gateways { for _, gw := range r.Gateways {
// record all the message ID's of the different bridges // record all the message ID's of the different bridges
var msgIDs []*BrMsgID var msgIDs []*BrMsgID
@@ -133,13 +135,25 @@ func (r *Router) handleReceive() {
} }
msg.Timestamp = time.Now() msg.Timestamp = time.Now()
gw.modifyMessage(&msg) gw.modifyMessage(&msg)
gw.handleFiles(&msg) if !filesHandled {
gw.handleFiles(&msg)
filesHandled = true
}
for _, br := range gw.Bridges { for _, br := range gw.Bridges {
msgIDs = append(msgIDs, gw.handleMessage(&msg, br)...) 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 != "" { if msg.ID != "" {
gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs) _, exists := gw.Messages.Get(msg.Protocol + " " + msg.ID)
// Only add the message ID if it doesn't already exist
//
// For some bridges we always add/update the message ID.
// This is necessary as msgIDs will change if a bridge returns
// a different ID in response to edits.
if !exists || msg.Protocol == "discord" {
gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs)
}
} }
} }
} }

31
go.mod
View File

@@ -3,27 +3,24 @@ module github.com/42wim/matterbridge
require ( require (
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 // indirect
github.com/Jeffail/gabs v1.1.1 // indirect github.com/Jeffail/gabs v1.1.1 // indirect
github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329
github.com/Rhymen/go-whatsapp v0.0.0-20190208184307-c9a81e957884 github.com/Rhymen/go-whatsapp v0.0.2
github.com/bwmarrin/discordgo v0.19.0 github.com/bwmarrin/discordgo v0.19.0
github.com/d5/tengo v1.9.2 github.com/d5/tengo v1.24.1
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec
github.com/fsnotify/fsnotify v1.4.7 github.com/fsnotify/fsnotify v1.4.7
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible 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/google/gops v0.3.6
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect
github.com/gorilla/schema v1.0.2 github.com/gorilla/schema v1.1.0
github.com/gorilla/websocket v1.4.0 github.com/gorilla/websocket v1.4.0
github.com/hashicorp/golang-lru v0.5.0 github.com/hashicorp/golang-lru v0.5.1
github.com/hpcloud/tail v1.0.0 // indirect github.com/hpcloud/tail v1.0.0 // indirect
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7
github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect github.com/labstack/echo/v4 v4.1.6
github.com/kr/pretty v0.1.0 // indirect
github.com/labstack/echo/v4 v4.0.0
github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398 github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect
@@ -41,33 +38,29 @@ require (
github.com/nlopes/slack v0.5.0 github.com/nlopes/slack v0.5.0
github.com/onsi/ginkgo v1.6.0 // indirect github.com/onsi/ginkgo v1.6.0 // indirect
github.com/onsi/gomega v1.4.1 // indirect github.com/onsi/gomega v1.4.1 // indirect
github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83 github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c
github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320 github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320
github.com/pkg/errors v0.8.0 // indirect
github.com/rs/xid v1.2.1 github.com/rs/xid v1.2.1
github.com/russross/blackfriday v2.0.0+incompatible github.com/russross/blackfriday v1.5.2
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca
github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296 github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296
github.com/sirupsen/logrus v1.3.0 github.com/sirupsen/logrus v1.4.2
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
github.com/spf13/viper v1.3.1 github.com/spf13/viper v1.4.0
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.3.0
github.com/technoweenie/multipartstreamer v1.0.1 // indirect github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447 github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a // indirect 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/linkify v0.0.0-20180917065525-c22b7bdb1179 // indirect
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f 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/mdurl v0.0.0-20180912090424-e5bce34c34f2 // indirect
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe // indirect gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe // indirect
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 // indirect gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 // indirect
go.uber.org/atomic v1.3.2 // indirect golang.org/x/image v0.0.0-20190616094056-33659d3de4f5
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.9.1 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
maunium.net/go/mautrix-whatsapp v0.0.0-20190127121751-281b3e8f77f3
) )

237
go.sum
View File

@@ -1,27 +1,40 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 h1:IZtuWGfzQnKnCSu+vl8WGLhpVQ5Uvy3rlSwqXSg+sQg= 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/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 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/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.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E= 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/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
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 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/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg=
github.com/Rhymen/go-whatsapp v0.0.0-20181218094654-2ca6af00572c h1:ldRXgMEfKmzBomrZusl3edG9AGEeztA7jovLEQy62us= github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA=
github.com/Rhymen/go-whatsapp v0.0.0-20181218094654-2ca6af00572c/go.mod h1:MSDmePOOkbFFbVW2WRRppBcbA+aabwpXRgyIIG7jDFQ= github.com/Rhymen/go-whatsapp v0.0.2 h1:MelwdquHuuNObBGV7CpXbky2aVdilx/CwiXMwZvS74U=
github.com/Rhymen/go-whatsapp v0.0.0-20190208184307-c9a81e957884 h1:2AxfzkQi2L4QGBvUCZoWD6hQuUJa5MG54wiYyNqJlf4= github.com/Rhymen/go-whatsapp v0.0.2/go.mod h1:qf/2PQi82Okxw/igghu/oMGzTeUYuKBq1JNo3tdQyNg=
github.com/Rhymen/go-whatsapp v0.0.0-20190208184307-c9a81e957884/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA= github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME=
github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58 h1:MkpmYfld/S8kXqTYI68DfL8/hHXjHogL120Dy00TIxc= github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU=
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw=
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM=
github.com/StackExchange/wmi v0.0.0-20170410192909-ea383cf3ba6e/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58/go.mod h1:YNfsMyWSs+h+PaYkxGeMVmVCX75Zj/pqdjbu12ciCYE= 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/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY= 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/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 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/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/d5/tengo v1.9.2 h1:UE/X8PYl7bLS4Ww2zGeh91nq5PTnkhe8ncgNeA5PK7k= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/d5/tengo v1.9.2/go.mod h1:gsbjo7lBXzBIWBd6NQp1lRKqqiDDANqBOyhW8rTlFsY= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/d5/tengo v1.24.1 h1:b+epGF5Qi0XUkYUUl8y6hVzLxg/eu9FYUAdb4H/KieY=
github.com/d5/tengo v1.24.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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -29,49 +42,74 @@ github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec h1:JEUiu7P9smN7zgX
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec/go.mod h1:UGa5M2Sz/Uh13AMse4+RELKCDw7kqgqlTjeGae+7vUY= 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 h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
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 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/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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gops v0.3.5 h1:SIWvPLiYvy5vMwjxB3rVFTE4QBhUFj2KKWr3Xm7CKhw= github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/google/gops v0.3.5/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/gops v0.3.6 h1:6akvbMlpZrEYOuoebn2kR+ZJekbZqJ28fJXTs84+8to=
github.com/google/gops v0.3.6/go.mod h1:RZ1rH95wsAGX4vMWKmqBOIWynmWisBf4QFdgT/k/xOI=
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 h1:4EZlYQIiyecYJlUbVkFXCXHz1QPhVXcHnQKAzBTPfQo= 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/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 h1:FDM3EtwZLyhW48YRiyqjivNlNZjAObv4xt4NnJaU+NQ=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA= github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
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 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 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/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/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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v1.3.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.3.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME= 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/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 h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro= 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/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/keybase/go-ps v0.0.0-20161005175911-668c8856d999 h1:2d+FLQbz4xRTi36DO1qYNUwfORax9XcQ0jhbO81Vago=
github.com/keybase/go-ps v0.0.0-20161005175911-668c8856d999/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.0.0 h1:q1GH+caIXPP7H2StPIdzy/ez9CO0EepqYeUg6vi9SWM= github.com/labstack/echo/v4 v4.1.6 h1:WOvLa4T1KzWCRpANwz0HGgWDelXSSGwIKtKBbFdHTv4=
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno= github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE=
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0= github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398 h1:a40kRmhA1p2XFJ6gqXfCExSyuDDCp/U9LA8ZY27u2Lk= 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/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 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU=
@@ -92,11 +130,15 @@ github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749
github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61/go.mod h1:iXGEotOvwI1R1SjLxRc+BF5rUORTMtE0iMZBT2lxqAU= github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61/go.mod h1:iXGEotOvwI1R1SjLxRc+BF5rUORTMtE0iMZBT2lxqAU=
github.com/mattermost/mattermost-server v5.5.0+incompatible h1:0wcLGgYtd+YImtLDPf2AOfpBHxbU4suATx+6XKw1XbU= 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/mattermost/mattermost-server v5.5.0+incompatible/go.mod h1:5L6MjAec+XXQwMIt791Ganu45GKsSiM+I0tLR9wUj8Y=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 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-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 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/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
@@ -105,50 +147,65 @@ github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 h1:oKIteT
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= 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 h1:HLGD5/9UxxfEuO9DtP8gnTmNtMxbPyhYltfxsITel8g=
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E= github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 h1:mp6tU1r0xLostUGLkTspf/9/AiHuVD7ptyXhySkDEsE= 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/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 h1:AgLl+Yq7kg5OYlzCgu9cKTZOyI4tD/NgukKqLqC8E+I=
github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0= github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0=
github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= 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/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U= github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83 h1:XQonH5Iv5rbyIkMJOQ4xKmKHQTh8viXtRSmep5Ca5I4= github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c h1:P6XGcuPTigoHf4TSu+3D/7QOQ1MbL6alNwrGhcW7sKw=
github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83/go.mod h1:YnNlZP7l4MhyGQ4CBRwv6ohZTPrUJJZtEv4ZgADkbs4= github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c/go.mod h1:YnNlZP7l4MhyGQ4CBRwv6ohZTPrUJJZtEv4ZgADkbs4=
github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 h1:/CPgDYrfeK2LMK6xcUhvI17yO9SlpAdDIJGkhDEgO8A= 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/pborman/uuid v0.0.0-20160216163710-c55201b03606/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 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/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 h1:YxcQy/DV+48NGv1lxx1vsWBzs6W1f1ogubkuCozxpX0=
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320/go.mod h1:G7LufuPajuIvdt9OitkNt2qh0mmvD4bfRgRM7bhDIOA= 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/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= 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/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 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 h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= 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 h1:Lx3BlDGFElJt4u/zKc9A3BuGYbQAGlEFyPuUA3jeMD0=
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI= github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI=
github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296 h1:8RLq547MSVc6vhOuCl4Ca0TsAQknj6NX6ZLSZ3+xmio= 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/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296/go.mod h1:1GLXsL4esywkpNId3v4QWuMf3THtWGitWvtQ/L3aSA4=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7 h1:80VN+vGkqM773Br/uNNTSheo3KatTgV8IpjIKjvVLng=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/skip2/go-qrcode v0.0.0-20171229120447-cf5f9fa2f0d8/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE= 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/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 h1:lXQ+j+KwZcbwrbgU0Rp4Eglg3EJLHbuZU3BbOqAGBmg=
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 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 h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 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/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 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
@@ -157,27 +214,31 @@ github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 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 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/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/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 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/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 v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= 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/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6 h1:YdYsPAZ2pC6Tow/nPZOPQ96O3hm/ToAkGsPLzedXERk=
github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 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-20190304061218-d34796e0a6c2 h1:UQwvu7FjUEdVYofx0U6bsc5odNE7wa5TSA0fl559GcA=
github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU= github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2/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 h1:Ax7kdHNICZiIeFpmevmaEWb0Ae3BUj3zCTKhZHZ+zd0=
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a/go.mod h1:JT4uoTz0tfPoyVH88GZoWDNm5NHJI2VbUW+eyPClueI= 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 h1:rbON2KwBnWuFMlSHM8LELLlwroDRZw6xv0e6il6e5dk=
@@ -190,34 +251,68 @@ gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe h1:5kUPFAF5
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe/go.mod h1:P9LSM1KVzrIstFgUaveuwiAm8PK5VTB3yJEU8kqlbrU= 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 h1:uPZaMiz6Sz0PZs3IZJWpU5qHKGNy///1pacZC9txiUI=
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638/go.mod h1:EGRJaqe2eO9XGmFtQCvV3Lm9NLico3UhFwUpCG/+mVU= gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638/go.mod h1:EGRJaqe2eO9XGmFtQCvV3Lm9NLico3UhFwUpCG/+mVU=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 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.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/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-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-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664 h1:YbZJ76lQ1BqNhVe7dKTSB67wDrc2VPRR75IyGyyPDX8=
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/image v0.0.0-20190616094056-33659d3de4f5 h1:ngW7cqsJcNIFizl289rKwy+nVvw7TQS8z3ejrra6syo=
golang.org/x/image v0.0.0-20190616094056-33659d3de4f5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190607181551-461777fb6f67 h1:rJJxsykSlULwd2P2+pg/rtnwN2FrWp4IuCxOSyS0V00=
golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 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/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20171017063910-8dbc5d05d6ed/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181212120007-b05ddf57801d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc h1:WiYx1rIFmx8c0mXAFtv5D/mHyKe1+jmuP7PViuwqwuQ= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190609082536-301114b31cce h1:CQakrGkKbydnUmt7cFIlmQ4lNQiqdTPt6xzXij4nYCc=
golang.org/x/sys v0.0.0-20190609082536-301114b31cce/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -225,13 +320,13 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 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 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
maunium.net/go/maulogger/v2 v2.0.0/go.mod h1:Hbbkq3NV6jvJodByZu1mgEF3fpT7Kz9z0MjEZ3/BusI= rsc.io/goversion v1.0.0 h1:/IhXBiai89TyuerPquiZZ39IQkTfAUbZB2awsyYZ/2c=
maunium.net/go/mautrix v0.1.0-alpha.3/go.mod h1:GTVu6WDHR+98DKOrYetWsXorvUeKQV3jsSWO6ScbuFI= rsc.io/goversion v1.0.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
maunium.net/go/mautrix-appservice v0.1.0-alpha.3/go.mod h1:wOnWOIuprYad7ly12rHIo3JLCPh4jwvx1prVrAB9RhM=
maunium.net/go/mautrix-whatsapp v0.0.0-20190127121751-281b3e8f77f3 h1:A18t5Lp7I3aK0V7B7zdpb0hb/PBlu0X/Ai2AyU/XEk4=
maunium.net/go/mautrix-whatsapp v0.0.0-20190127121751-281b3e8f77f3/go.mod h1:r5E3J4urDEsjfui9OYZYMLBfCliaAqcCwM2xeczta6k=

288
internal/bindata.go Normal file
View File

@@ -0,0 +1,288 @@
// Code generated by go-bindata. DO NOT EDIT.
// sources:
// tengo/outmessage.tengo
package internal
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info fileInfoEx
}
type fileInfoEx interface {
os.FileInfo
MD5Checksum() string
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
md5checksum string
}
func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) MD5Checksum() string {
return fi.md5checksum
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var _bindataTengoOutmessagetengo = []byte(
"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x91\x3d\x8f\xda\x40\x10\x86\xfb\xfd\x15\x13\x37\xb1\x2d\x07\xe7\xa3" +
"\xb3\x64\x59\x11\x45\x94\x2e\x8a\x92\x0a\xd0\xb1\xac\x07\x33\xd2\x7a\xc7\x1a\x8f\x31\x88\xe3\xbf\x9f\xcc\x01\x47" +
"\x7f\xc5\x75\xef\xae\x9e\x9d\x77\x1f\x4d\x9e\x9a\xbd\x15\xb2\x1b\x8f\x3d\xd8\xbd\x25\x3f\x45\x30\x82\xb6\xfe\xc2" +
"\xc1\x1f\x0b\x43\xe1\xa7\x73\x3c\x04\xcd\x80\xc2\x1f\x61\x65\xc7\x7e\xca\xf3\x9d\x0d\x01\x2f\xf1\x97\x55\x1c\xed" +
"\xd1\xf0\xa0\x77\x98\x07\x7d\xa3\x79\xd0\x3b\xce\x83\xde\xf8\xd7\x9e\x51\x48\xb1\x30\x6d\xdf\xfc\xc3\x83\x66\xd0" +
"\xf6\xcd\xff\x1e\x25\xd8\x16\x4d\x9a\x1b\xa3\x78\x50\x28\x4a\xa0\xb6\x63\xd1\x38\x9a\xce\x51\x62\x4c\x9e\x43\xaf" +
"\x42\x1d\x90\x38\x70\xec\x59\xfa\xe9\x8e\xb6\x30\xe2\x67\x41\x08\xac\xd0\x63\xa8\x29\x34\xa0\x0c\x36\x5c\xc0\x8d" +
"\x50\xdd\x20\x8c\x78\x7d\xac\x3b\x84\xdf\x7f\xe7\xb7\x01\xb4\x7d\xd0\x84\xb2\x84\x88\xc4\x45\x70\x32\x00\x00\x82" +
"\xd3\x3f\xa6\xfe\x99\xe0\x93\xe3\xb6\x23\x8f\xf1\x7a\x79\xf8\xfa\x23\xae\x8a\x65\x7d\xfa\x96\x7d\x3f\xc7\x55\x91" +
"\x5d\x63\x52\x25\xd5\xf3\x62\x51\xb8\xa0\xe2\x8b\xd5\x6a\x9d\x5c\xc6\x5c\x4d\x4b\xc1\x99\x60\xe7\xad\xc3\xf8\x26" +
"\x1f\x45\x89\x39\x9b\xf7\x6b\xe4\x29\x6d\x1f\x57\x00\x9f\x3e\xc6\x24\xcd\xcd\x4b\x00\x00\x00\xff\xff\x40\xb8\x54" +
"\xb8\x64\x02\x00\x00")
func bindataTengoOutmessagetengoBytes() ([]byte, error) {
return bindataRead(
_bindataTengoOutmessagetengo,
"tengo/outmessage.tengo",
)
}
func bindataTengoOutmessagetengo() (*asset, error) {
bytes, err := bindataTengoOutmessagetengoBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{
name: "tengo/outmessage.tengo",
size: 612,
md5checksum: "",
mode: os.FileMode(420),
modTime: time.Unix(1555622139, 0),
}
a := &asset{bytes: bytes, info: info}
return a, nil
}
//
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
//
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
//
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
// nolint: deadcode
//
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
//
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or could not be loaded.
//
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
//
// AssetNames returns the names of the assets.
// nolint: deadcode
//
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
//
// _bindata is a table, holding each asset generator, mapped to its name.
//
var _bindata = map[string]func() (*asset, error){
"tengo/outmessage.tengo": bindataTengoOutmessagetengo,
}
//
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
//
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
}
}
if node.Func != nil {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{Func: nil, Children: map[string]*bintree{
"tengo": {Func: nil, Children: map[string]*bintree{
"outmessage.tengo": {Func: bindataTengoOutmessagetengo, Children: map[string]*bintree{}},
}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}

View File

@@ -0,0 +1,19 @@
/*
variables available
read-only:
inAccount, inProtocol, inChannel, inGateway
outAccount, outProtocol, outChannel, outGateway
read-write:
msgText, msgUsername
*/
text := import("text")
// start - strip irc colors
// if we're not sending to an irc bridge we strip the IRC colors
if inProtocol == "irc" {
re := text.re_compile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
msgText=re.replace(msgText,"")
}
// end - strip irc colors

View File

@@ -15,7 +15,7 @@ import (
) )
var ( var (
version = "1.14.0-rc1" version = "1.15.1"
githash string githash string
flagConfig = flag.String("conf", "matterbridge.toml", "config file") flagConfig = flag.String("conf", "matterbridge.toml", "config file")

View File

@@ -1,5 +1,7 @@
#This is configuration for matterbridge. #This is configuration for matterbridge.
#WARNING: as this file contains credentials, be sure to set correct file permissions #WARNING: as this file contains credentials, be sure to set correct file permissions
#See https://github.com/42wim/matterbridge/wiki/How-to-create-your-config for how to create your config
#See https://github.com/42wim/matterbridge/wiki/Settings for all settings
################################################################### ###################################################################
#IRC section #IRC section
################################################################### ###################################################################
@@ -27,7 +29,7 @@ UseTLS=false
#OPTIONAL (default false) #OPTIONAL (default false)
UseSASL=false UseSASL=false
#Enable to not verify the certificate on your irc server. i #Enable to not verify the certificate on your irc server.
#e.g. when using selfsigned certificates #e.g. when using selfsigned certificates
#OPTIONAL (default false) #OPTIONAL (default false)
SkipTLSVerify=true SkipTLSVerify=true
@@ -155,6 +157,11 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
#Enable to show verbose users joins/parts (ident@host) from other bridges
#Currently works for messages from the following bridges: irc
#OPTIONAL (default false)
VerboseJoinPart=false
#Do not send joins/parts to other bridges #Do not send joins/parts to other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack #Currently works for messages from the following bridges: irc, mattermost, slack
#OPTIONAL (default false) #OPTIONAL (default false)
@@ -270,98 +277,6 @@ StripNick=false
#OPTIONAL (default false) #OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
###################################################################
#hipchat section
###################################################################
#Go to https://www.hipchat.com/account/xmpp this will show you the necessary data
#to fill in the section below
[xmpp.hipchat]
#xmpp server to connect to.
#REQUIRED
Server="chat.hipchat.com:5222"
#Jabber ID
#REQUIRED
Jid="12345_12345@chat.hipchat.com"
#Password (your hipchat password)
#REQUIRED
Password="yourpass"
#Conference (MUC) domain
#REQUIRED
Muc="conf.hipchat.com"
#Room nickname
#REQUIRED
Nick="yourlogin"
## 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"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#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=""
#RemoteNickFormat defines how remote users appear on this bridge
#See [general] config section for default options
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
#It will strip other characters from the nick
#OPTIONAL (default false)
StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
#mattermost section #mattermost section
################################################################### ###################################################################
@@ -435,6 +350,12 @@ NickFormatter="plain"
#OPTIONAL (default 4) #OPTIONAL (default 4)
NicksPerRow=4 NicksPerRow=4
#Skip the Mattermost server version checks that are normally done when connecting.
#The usage scenario for this feature would be when the Mattermost instance is hosted behind a
#reverse proxy that suppresses "non-standard" response headers in flight.
#OPTIONAL (default false)
SkipVersionCheck=false
#Whether to prefix messages from other bridges to mattermost with the sender's nick. #Whether to prefix messages from other bridges to mattermost with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the #Useful if username overrides for incoming webhooks isn't enabled on the
#mattermost server. If you set PrefixMessagesWithNick to true, each message #mattermost server. If you set PrefixMessagesWithNick to true, each message
@@ -913,6 +834,11 @@ QuoteDisable=false
#OPTIONAL (default "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})") #OPTIONAL (default "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})")
QuoteFormat="{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 #Disable sending of edits to other bridges
#OPTIONAL (default false) #OPTIONAL (default false)
EditDisable=false EditDisable=false
@@ -1002,9 +928,10 @@ ShowTopicChange=false
Server="https://yourrocketchatserver.domain.com:443" Server="https://yourrocketchatserver.domain.com:443"
#login/pass of your bot. #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! #Use a dedicated user for this and not your own!
#REQUIRED (when not using webhooks) #REQUIRED (when not using webhooks)
Login="yourlogin" Login="yourlogin@domain.com"
Password="yourpass" Password="yourpass"
#### Settings for webhook matterbridge. #### Settings for webhook matterbridge.
@@ -1045,6 +972,8 @@ SkipTLSVerify=true
#Useful if username overrides for incoming webhooks isn't enabled on the #Useful if username overrides for incoming webhooks isn't enabled on the
#rocketchat server. If you set PrefixMessagesWithNick to true, each message #rocketchat server. If you set PrefixMessagesWithNick to true, each message
#from bridge to rocketchat will by default be prefixed by the RemoteNickFormat setting. i #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) #OPTIONAL (default false)
PrefixMessagesWithNick=false PrefixMessagesWithNick=false
@@ -1358,12 +1287,6 @@ Login="yourbot-bot@yourserver.zulipchat.com"
#REQUIRED #REQUIRED
Server="https://yourserver.zulipchat.com" Server="https://yourserver.zulipchat.com"
#Topic of the messages matterbridge will use
#OPTIONAL (default "matterbridge")
#You can specify a specific topic for each channel using [gateway.inout.options]
#See more information below at the gateway configuration
Topic="matterbridge"
## RELOADABLE SETTINGS ## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file ## Settings below can be reloaded by editing the file
@@ -1478,6 +1401,7 @@ RemoteNickFormat="{NICK}"
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
#The string "{GATEWAY}" (case sensitive) will be replaced by the origin gateway name that is replicating the message. #The string "{GATEWAY}" (case sensitive) will be replaced by the origin gateway name that is replicating the message.
#The string "{CHANNEL}" (case sensitive) will be replaced by the origin channel name used by the bridge #The string "{CHANNEL}" (case sensitive) will be replaced by the origin channel name used by the bridge
#The string "{TENGO}" (case sensitive) will be replaced by the output of the RemoteNickFormat script under [tengo]
#OPTIONAL (default empty) #OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
@@ -1527,8 +1451,14 @@ MediaDownloadBlacklist=[".html$",".htm$"]
#OPTIONAL (default false) #OPTIONAL (default false)
IgnoreFailureOnStart=false IgnoreFailureOnStart=false
###################################################################
#Tengo configuration
###################################################################
#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
#TengoModifyMessage allows you to specify the location of a tengo (https://github.com/d5/tengo/) script. [tengo]
#InMessage 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. #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: #The script will have the following global variables:
#to modify: msgUsername and msgText #to modify: msgUsername and msgText
@@ -1545,10 +1475,42 @@ IgnoreFailureOnStart=false
# msgText="replaced by this" # msgText="replaced by this"
# msgUsername="fakeuser" # 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) #OPTIONAL (default empty)
TengoModifyMessage="example.tengo" InMessage="example.tengo"
#OutMessage allows you to specify the location of the script that
#will be invoked on each message being sent to a bridge and can be used to modify the Username
#and the Text of that message.
#
#The script will have the following global variables:
#read-only:
#inAccount, inProtocol, inChannel, inGateway, inEvent
#outAccount, outProtocol, outChannel, outGateway, outEvent
#
#read-write:
#msgText, msgUsername
#
#The script is reloaded on every message, so you can modify the script on the fly.
#
#The default script in https://github.com/42wim/matterbridge/tree/master/internal/tengo/outmessage.tengo
#is compiled in and will be executed if no script is specified.
#OPTIONAL (default empty)
OutMessage="example.tengo"
#RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script.
#The script will have the following global variables:
#to modify: result
#to read: channel, bridge, gateway, protocol, nick
#
#The result will be set in {TENGO} in the RemoteNickFormat key of every bridge where {TENGO} is specified
#
#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/contrib/remotenickformat.tengo
#
#OPTIONAL (default empty)
RemoteNickFormat="remotenickformat.tengo"
################################################################### ###################################################################
#Gateway configuration #Gateway configuration
@@ -1591,6 +1553,7 @@ enable=true
# discord - channel (without the #) # discord - channel (without the #)
# - ID:123456789 (where 123456789 is the channel ID) # - ID:123456789 (where 123456789 is the channel ID)
# (https://github.com/42wim/matterbridge/issues/57) # (https://github.com/42wim/matterbridge/issues/57)
# - category/channel (without the #) if you're using discord categories to group your channels
# 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) # 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) # hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
@@ -1603,7 +1566,7 @@ enable=true
# if you specify an empty string bridge will list all the possibilities # 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 # - "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 # as group names might change in time and contain weird emoticons
# zulip - stream (without the #) # zulip - stream/topic:topicname (without the #)
# #
# REQUIRED # REQUIRED
channel="#testing" channel="#testing"
@@ -1645,10 +1608,7 @@ enable=true
[[gateway.inout]] [[gateway.inout]]
account="zulip.streamchat" account="zulip.streamchat"
channel="general" channel="general/topic:mytopic"
#OPTIONAL - topic only works for zulip
[gateway.inout.options]
topic="topic1"
#API example #API example
#[[gateway.inout]] #[[gateway.inout]]

View File

@@ -51,8 +51,9 @@ func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:go
if res == name { if res == name {
return channel.Id return channel.Id
} }
} else if channel.Name == name {
return channel.Id
} }
} }
} }
return "" return ""
@@ -166,23 +167,42 @@ func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint
return nil return nil
} }
func (m *MMClient) UpdateChannelsTeam(teamID string) error {
mmchannels, resp := m.Client.GetChannelsForTeamForUser(teamID, m.User.Id, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
for idx, t := range m.OtherTeams {
if t.Id == teamID {
m.Lock()
m.OtherTeams[idx].Channels = mmchannels
m.Unlock()
}
}
mmchannels, resp = m.Client.GetPublicChannelsForTeam(teamID, 0, 5000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
for idx, t := range m.OtherTeams {
if t.Id == teamID {
m.Lock()
m.OtherTeams[idx].MoreChannels = mmchannels
m.Unlock()
}
}
return nil
}
func (m *MMClient) UpdateChannels() error { func (m *MMClient) UpdateChannels() error {
mmchannels, resp := m.Client.GetChannelsForTeamForUser(m.Team.Id, m.User.Id, "") if err := m.UpdateChannelsTeam(m.Team.Id); err != nil {
if resp.Error != nil { return err
return errors.New(resp.Error.DetailedError)
} }
m.Lock() for _, t := range m.OtherTeams {
m.Team.Channels = mmchannels if err := m.UpdateChannelsTeam(t.Id); err != nil {
m.Unlock() return err
}
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 return nil
} }

View File

@@ -132,14 +132,25 @@ func (m *MMClient) initUser() error {
return resp.Error return resp.Error
} }
for _, team := range teams { for _, team := range teams {
mmusers, resp := m.Client.GetUsersInTeam(team.Id, 0, 50000, "") idx := 0
max := 200
usermap := make(map[string]*model.User)
mmusers, resp := m.Client.GetUsersInTeam(team.Id, idx, max, "")
if resp.Error != nil { if resp.Error != nil {
return errors.New(resp.Error.DetailedError) return errors.New(resp.Error.DetailedError)
} }
usermap := make(map[string]*model.User) for len(mmusers) > 0 {
for _, user := range mmusers { for _, user := range mmusers {
usermap[user.Id] = user usermap[user.Id] = user
}
mmusers, resp = m.Client.GetUsersInTeam(team.Id, idx, max, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
idx++
time.Sleep(time.Millisecond * 200)
} }
m.logger.Infof("found %d users in team %s", len(usermap), team.Name)
t := &Team{Team: team, Users: usermap, Id: team.Id} t := &Team{Team: team, Users: usermap, Id: team.Id}
@@ -175,15 +186,19 @@ func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error {
if resp.Error != nil { if resp.Error != nil {
return fmt.Errorf("%#v", resp.Error.Error()) return fmt.Errorf("%#v", resp.Error.Error())
} }
if firstConnection && !supportedVersion(resp.ServerVersion) { if firstConnection && !m.SkipVersionCheck && !supportedVersion(resp.ServerVersion) {
return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion) return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion)
} }
m.ServerVersion = resp.ServerVersion if !m.SkipVersionCheck {
if m.ServerVersion == "" { m.ServerVersion = resp.ServerVersion
m.logger.Debugf("Server not up yet, reconnecting in %s", d) if m.ServerVersion == "" {
time.Sleep(d) 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
}
} else { } else {
m.logger.Infof("Found version %s", m.ServerVersion)
return nil return nil
} }
} }

View File

@@ -16,14 +16,15 @@ import (
) )
type Credentials struct { type Credentials struct {
Login string Login string
Team string Team string
Pass string Pass string
Token string Token string
CookieToken bool CookieToken bool
Server string Server string
NoTLS bool NoTLS bool
SkipTLSVerify bool SkipTLSVerify bool
SkipVersionCheck bool
} }
type Message struct { type Message struct {
@@ -216,9 +217,17 @@ func (m *MMClient) WsReceiver() {
if msg.Post != nil { if msg.Post != nil {
if msg.Text != "" || len(msg.Post.FileIds) > 0 || msg.Post.Type == "slack_attachment" { if msg.Text != "" || len(msg.Post.FileIds) > 0 || msg.Post.Type == "slack_attachment" {
m.MessageChan <- msg m.MessageChan <- msg
continue
} }
} }
continue switch msg.Raw.Event {
case model.WEBSOCKET_EVENT_USER_ADDED,
model.WEBSOCKET_EVENT_USER_REMOVED,
model.WEBSOCKET_EVENT_CHANNEL_CREATED,
model.WEBSOCKET_EVENT_CHANNEL_DELETED:
m.MessageChan <- msg
continue
}
} }
var response model.WebSocketResponse var response model.WebSocketResponse

View File

@@ -83,7 +83,7 @@ func (m *MMClient) DeleteMessage(postId string) error { //nolint:golint
} }
func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint
post := &model.Post{Message: text} post := &model.Post{Message: text, Id: postId}
res, resp := m.Client.UpdatePost(postId, post) res, resp := m.Client.UpdatePost(postId, post)
if resp.Error != nil { if resp.Error != nil {
return "", resp.Error return "", resp.Error

View File

@@ -2,6 +2,7 @@ package matterclient
import ( import (
"errors" "errors"
"time"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
) )
@@ -99,15 +100,25 @@ func (m *MMClient) GetUsers() map[string]*model.User {
} }
func (m *MMClient) UpdateUsers() error { func (m *MMClient) UpdateUsers() error {
mmusers, resp := m.Client.GetUsers(0, 50000, "") idx := 0
max := 200
mmusers, resp := m.Client.GetUsers(idx, max, "")
if resp.Error != nil { if resp.Error != nil {
return errors.New(resp.Error.DetailedError) return errors.New(resp.Error.DetailedError)
} }
m.Lock() for len(mmusers) > 0 {
for _, user := range mmusers { m.Lock()
m.Users[user.Id] = user for _, user := range mmusers {
m.Users[user.Id] = user
}
m.Unlock()
mmusers, resp = m.Client.GetUsers(idx, max, "")
time.Sleep(time.Millisecond * 300)
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
idx++
} }
m.Unlock()
return nil return nil
} }

View File

@@ -1,2 +1,3 @@
.idea/ .idea/
docs/ docs/
build/

View File

@@ -3,7 +3,7 @@ Package rhymen/go-whatsapp implements the WhatsApp Web API to provide a clean in
## Installation ## Installation
```sh ```sh
go get github.com/rhymen/go-whatsapp go get github.com/Rhymen/go-whatsapp
``` ```
## Usage ## Usage
@@ -30,7 +30,7 @@ The authentication process requires you to scan the qr code, that is send throug
### Restore ### Restore
```go ```go
newSess, err := wac.RestoreSession(sess) newSess, err := wac.RestoreWithSession(sess)
``` ```
The restore function needs a valid session and returns the new session that was created. The restore function needs a valid session and returns the new session that was created.

View File

@@ -2,21 +2,13 @@
package whatsapp package whatsapp
import ( import (
"crypto/hmac"
"crypto/sha256"
"encoding/json"
"fmt"
"math/rand" "math/rand"
"net/http" "net/http"
"os"
"strconv"
"strings"
"sync" "sync"
"time" "time"
"github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/crypto/cbc"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/pkg/errors"
) )
type metric byte type metric byte
@@ -79,13 +71,15 @@ Conn is created by NewConn. Interacting with the initialized Conn is the main wa
It holds all necessary information to make the package work internally. It holds all necessary information to make the package work internally.
*/ */
type Conn struct { type Conn struct {
wsConn *websocket.Conn ws *websocketWrapper
wsConnOK bool listener *listenerWrapper
wsConnMutex sync.RWMutex
connected bool
loggedIn bool
wg *sync.WaitGroup
session *Session session *Session
listener map[string]chan string sessionLock uint32
listenerMutex sync.RWMutex
writeChan chan wsMsg
handler []Handler handler []Handler
msgCount int msgCount int
msgTimeout time.Duration msgTimeout time.Duration
@@ -97,9 +91,15 @@ type Conn struct {
shortClientName string shortClientName string
} }
type wsMsg struct { type websocketWrapper struct {
messageType int sync.Mutex
data []byte conn *websocket.Conn
close chan struct{}
}
type listenerWrapper struct {
sync.RWMutex
m map[string]chan string
} }
/* /*
@@ -108,50 +108,29 @@ The goroutine for handling incoming messages is started
*/ */
func NewConn(timeout time.Duration) (*Conn, error) { func NewConn(timeout time.Duration) (*Conn, error) {
wac := &Conn{ wac := &Conn{
wsConn: nil, // will be set in connect() handler: make([]Handler, 0),
wsConnMutex: sync.RWMutex{}, msgCount: 0,
listener: make(map[string]chan string), msgTimeout: timeout,
listenerMutex: sync.RWMutex{}, Store: newStore(),
writeChan: make(chan wsMsg),
handler: make([]Handler, 0),
msgCount: 0,
msgTimeout: timeout,
Store: newStore(),
longClientName: "github.com/rhymen/go-whatsapp", longClientName: "github.com/rhymen/go-whatsapp",
shortClientName: "go-whatsapp", shortClientName: "go-whatsapp",
} }
return wac, wac.connect()
if err := wac.connect(); err != nil {
return nil, err
}
go wac.readPump()
go wac.writePump()
go wac.keepAlive(20000, 90000)
return wac, nil
} }
func (wac *Conn) isConnected() bool { // connect should be guarded with wsWriteMutex
wac.wsConnMutex.RLock() func (wac *Conn) connect() (err error) {
defer wac.wsConnMutex.RUnlock() if wac.connected {
if wac.wsConn == nil { return ErrAlreadyConnected
return false
}
if wac.wsConnOK {
return true
} }
wac.connected = true
defer func() { // set connected to false on error
if err != nil {
wac.connected = false
}
}()
// just send a keepalive to test the connection
wac.sendKeepAlive()
// this method is expected to be called by loops. So we can just return false
return false
}
// connect should be guarded with wsConnMutex
func (wac *Conn) connect() error {
dialer := &websocket.Dialer{ dialer := &websocket.Dialer{
ReadBufferSize: 25 * 1024 * 1024, ReadBufferSize: 25 * 1024 * 1024,
WriteBufferSize: 10 * 1024 * 1024, WriteBufferSize: 10 * 1024 * 1024,
@@ -161,229 +140,71 @@ func (wac *Conn) connect() error {
headers := http.Header{"Origin": []string{"https://web.whatsapp.com"}} headers := http.Header{"Origin": []string{"https://web.whatsapp.com"}}
wsConn, _, err := dialer.Dial("wss://web.whatsapp.com/ws", headers) wsConn, _, err := dialer.Dial("wss://web.whatsapp.com/ws", headers)
if err != nil { if err != nil {
return fmt.Errorf("couldn't dial whatsapp web websocket: %v", err) return errors.Wrap(err, "couldn't dial whatsapp web websocket")
} }
wsConn.SetCloseHandler(func(code int, text string) error { wsConn.SetCloseHandler(func(code int, text string) error {
fmt.Fprintf(os.Stderr, "websocket connection closed(%d, %s)\n", code, text)
// from default CloseHandler // from default CloseHandler
message := websocket.FormatCloseMessage(code, "") message := websocket.FormatCloseMessage(code, "")
wsConn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second)) err := wsConn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
// our close handling // our close handling
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { _, _ = wac.Disconnect()
fmt.Println("Trigger reconnect") wac.handle(&ErrConnectionClosed{Code: code, Text: text})
go wac.reconnect() return err
}
return nil
}) })
wac.wsConn = wsConn wac.ws = &websocketWrapper{
wac.wsConnOK = true conn: wsConn,
close: make(chan struct{}),
}
wac.listener = &listenerWrapper{
m: make(map[string]chan string),
}
wac.wg = &sync.WaitGroup{}
wac.wg.Add(2)
go wac.readPump()
go wac.keepAlive(20000, 60000)
wac.loggedIn = false
return nil return nil
} }
// reconnect should be run as go routine func (wac *Conn) Disconnect() (Session, error) {
func (wac *Conn) reconnect() { if !wac.connected {
wac.wsConnMutex.Lock() return Session{}, ErrNotConnected
wac.wsConn.Close()
wac.wsConn = nil
wac.wsConnOK = false
wac.wsConnMutex.Unlock()
// wait up to 60 seconds and then reconnect. As writePump should send immediately, it might
// reconnect as well. So we check its existance before reconnecting
for !wac.isConnected() {
time.Sleep(time.Duration(rand.Intn(60)) * time.Second)
wac.wsConnMutex.Lock()
if wac.wsConn == nil {
if err := wac.connect(); err != nil {
fmt.Fprintf(os.Stderr, "could not reconnect to websocket: %v\n", err)
}
}
wac.wsConnMutex.Unlock()
} }
} wac.connected = false
wac.loggedIn = false
func (wac *Conn) write(data []interface{}) (<-chan string, error) { close(wac.ws.close) //signal close
d, err := json.Marshal(data) wac.wg.Wait() //wait for close
if err != nil {
return nil, err err := wac.ws.conn.Close()
} wac.ws = nil
ts := time.Now().Unix() if wac.session == nil {
messageTag := fmt.Sprintf("%d.--%d", ts, wac.msgCount) return Session{}, err
msg := fmt.Sprintf("%s,%s", messageTag, d)
ch := make(chan string, 1)
wac.listenerMutex.Lock()
wac.listener[messageTag] = ch
wac.listenerMutex.Unlock()
wac.writeChan <- wsMsg{websocket.TextMessage, []byte(msg)}
wac.msgCount++
return ch, nil
}
func (wac *Conn) writeBinary(node binary.Node, metric metric, flag flag, tag string) (<-chan string, error) {
if len(tag) < 2 {
return nil, fmt.Errorf("no tag specified or to short")
}
b, err := binary.Marshal(node)
if err != nil {
return nil, err
}
cipher, err := cbc.Encrypt(wac.session.EncKey, nil, b)
if err != nil {
return nil, err
}
h := hmac.New(sha256.New, wac.session.MacKey)
h.Write(cipher)
hash := h.Sum(nil)
data := []byte(tag + ",")
data = append(data, byte(metric), byte(flag))
data = append(data, hash[:32]...)
data = append(data, cipher...)
ch := make(chan string, 1)
wac.listenerMutex.Lock()
wac.listener[tag] = ch
wac.listenerMutex.Unlock()
msg := wsMsg{websocket.BinaryMessage, data}
wac.writeChan <- msg
wac.msgCount++
return ch, nil
}
func (wac *Conn) readPump() {
defer wac.wsConn.Close()
for {
msgType, msg, err := wac.wsConn.ReadMessage()
if err != nil {
wac.wsConnOK = false
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
wac.handle(fmt.Errorf("unexpected websocket close: %v", err))
}
// sleep for a second and retry reading the next message
time.Sleep(time.Second)
continue
}
wac.wsConnOK = true
data := strings.SplitN(string(msg), ",", 2)
//Kepp-Alive Timestmap
if data[0][0] == '!' {
msecs, err := strconv.ParseInt(data[0][1:], 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error converting time string to uint: %v\n", err)
continue
}
wac.ServerLastSeen = time.Unix(msecs/1000, (msecs%1000)*int64(time.Millisecond))
continue
}
wac.listenerMutex.RLock()
listener, hasListener := wac.listener[data[0]]
wac.listenerMutex.RUnlock()
if len(data[1]) == 0 {
continue
} else if hasListener {
listener <- data[1]
wac.listenerMutex.Lock()
delete(wac.listener, data[0])
wac.listenerMutex.Unlock()
} else if msgType == 2 && wac.session != nil && wac.session.EncKey != nil {
message, err := wac.decryptBinaryMessage([]byte(data[1]))
if err != nil {
wac.handle(fmt.Errorf("error decoding binary: %v", err))
continue
}
wac.dispatch(message)
} else {
wac.handle(string(data[1]))
}
}
}
func (wac *Conn) writePump() {
for msg := range wac.writeChan {
for !wac.isConnected() {
// reconnect to send the message ASAP
wac.wsConnMutex.Lock()
if wac.wsConn == nil {
if err := wac.connect(); err != nil {
fmt.Fprintf(os.Stderr, "could not reconnect to websocket: %v\n", err)
}
}
wac.wsConnMutex.Unlock()
if !wac.isConnected() {
// reconnecting failed. Sleep for a while and try again afterwards
time.Sleep(time.Duration(rand.Intn(5)) * time.Second)
}
}
if err := wac.wsConn.WriteMessage(msg.messageType, msg.data); err != nil {
fmt.Fprintf(os.Stderr, "error writing to socket: %v\n", err)
wac.wsConnOK = false
// add message to channel again to no loose it
go func() {
wac.writeChan <- msg
}()
}
}
}
func (wac *Conn) sendKeepAlive() {
// whatever issues might be there allow sending this message
wac.wsConnOK = true
wac.writeChan <- wsMsg{
messageType: websocket.TextMessage,
data: []byte("?,,"),
} }
return *wac.session, err
} }
func (wac *Conn) keepAlive(minIntervalMs int, maxIntervalMs int) { func (wac *Conn) keepAlive(minIntervalMs int, maxIntervalMs int) {
defer wac.wg.Done()
for { for {
wac.sendKeepAlive() err := wac.sendKeepAlive()
if err != nil {
wac.handle(errors.Wrap(err, "keepAlive failed"))
//TODO: Consequences?
}
interval := rand.Intn(maxIntervalMs-minIntervalMs) + minIntervalMs interval := rand.Intn(maxIntervalMs-minIntervalMs) + minIntervalMs
<-time.After(time.Duration(interval) * time.Millisecond) select {
case <-time.After(time.Duration(interval) * time.Millisecond):
case <-wac.ws.close:
return
}
} }
} }
func (wac *Conn) decryptBinaryMessage(msg []byte) (*binary.Node, error) {
//message validation
h2 := hmac.New(sha256.New, wac.session.MacKey)
h2.Write([]byte(msg[32:]))
if !hmac.Equal(h2.Sum(nil), msg[:32]) {
return nil, fmt.Errorf("message received with invalid hmac")
}
// message decrypt
d, err := cbc.Decrypt(wac.session.EncKey, nil, msg[32:])
if err != nil {
return nil, fmt.Errorf("error decrypting message with AES: %v", err)
}
// message unmarshal
message, err := binary.Unmarshal(d)
if err != nil {
return nil, fmt.Errorf("error decoding binary: %v", err)
}
return message, nil
}

View File

@@ -13,59 +13,26 @@ const (
PresenceAvailable = "available" PresenceAvailable = "available"
PresenceUnavailable = "unavailable" PresenceUnavailable = "unavailable"
PresenceComposing = "composing" PresenceComposing = "composing"
PresenceRecording = "recording" PresenceRecording = "recording"
PresencePaused = "paused" PresencePaused = "paused"
) )
//TODO: filename? WhatsApp uses Store.Contacts for these functions //TODO: filename? WhatsApp uses Store.Contacts for these functions
//TODO: functions probably shouldn't return a string, maybe build a struct / return json // functions probably shouldn't return a string, maybe build a struct / return json
//TODO: check for further queries // check for further queries
func (wac *Conn) GetProfilePicThumb(jid string) (<-chan string, error) { func (wac *Conn) GetProfilePicThumb(jid string) (<-chan string, error) {
data := []interface{}{"query", "ProfilePicThumb", jid} data := []interface{}{"query", "ProfilePicThumb", jid}
return wac.write(data) return wac.writeJson(data)
} }
func (wac *Conn) GetStatus(jid string) (<-chan string, error) { func (wac *Conn) GetStatus(jid string) (<-chan string, error) {
data := []interface{}{"query", "Status", jid} data := []interface{}{"query", "Status", jid}
return wac.write(data) return wac.writeJson(data)
}
func (wac *Conn) GetGroupMetaData(jid string) (<-chan string, error) {
data := []interface{}{"query", "GroupMetadata", jid}
return wac.write(data)
} }
func (wac *Conn) SubscribePresence(jid string) (<-chan string, error) { func (wac *Conn) SubscribePresence(jid string) (<-chan string, error) {
data := []interface{}{"action", "presence", "subscribe", jid} data := []interface{}{"action", "presence", "subscribe", jid}
return wac.write(data) return wac.writeJson(data)
}
func (wac *Conn) CreateGroup(subject string, participants []string) (<-chan string, error) {
return wac.setGroup("create", "", subject, participants)
}
func (wac *Conn) UpdateGroupSubject(subject string, jid string) (<-chan string, error) {
return wac.setGroup("subject", jid, subject, nil)
}
func (wac *Conn) SetAdmin(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("promote", jid, "", participants)
}
func (wac *Conn) RemoveAdmin(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("demote", jid, "", participants)
}
func (wac *Conn) AddMember(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("add", jid, "", participants)
}
func (wac *Conn) RemoveMember(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("remove", jid, "", participants)
}
func (wac *Conn) LeaveGroup(jid string) (<-chan string, error) {
return wac.setGroup("leave", jid, "", nil)
} }
func (wac *Conn) Search(search string, count, page int) (*binary.Node, error) { func (wac *Conn) Search(search string, count, page int) (*binary.Node, error) {
@@ -117,7 +84,7 @@ func (wac *Conn) Presence(jid string, presence Presence) (<-chan string, error)
func (wac *Conn) Exist(jid string) (<-chan string, error) { func (wac *Conn) Exist(jid string) (<-chan string, error) {
data := []interface{}{"query", "exist", jid} data := []interface{}{"query", "exist", jid}
return wac.write(data) return wac.writeJson(data)
} }
func (wac *Conn) Emoji() (*binary.Node, error) { func (wac *Conn) Emoji() (*binary.Node, error) {

View File

@@ -9,7 +9,6 @@ second stage "expands" this key into several additional pseudorandom keys (the o
package hkdf package hkdf
import ( import (
"crypto/hmac"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
@@ -20,33 +19,29 @@ import (
Expand expands a given key with the HKDF algorithm. Expand expands a given key with the HKDF algorithm.
*/ */
func Expand(key []byte, length int, info string) ([]byte, error) { func Expand(key []byte, length int, info string) ([]byte, error) {
var h io.Reader
if info == "" { if info == "" {
keyBlock := hmac.New(sha256.New, key) /*
var out, last []byte Only used during initial login
Pseudorandom Key is provided by server and has not to be created
var blockIndex byte = 1 */
for i := 0; len(out) < length; i++ { h = hkdf.Expand(sha256.New, key, []byte(info))
keyBlock.Reset()
//keyBlock.Write(append(append(last, []byte(info)...), blockIndex))
keyBlock.Write(last)
keyBlock.Write([]byte(info))
keyBlock.Write([]byte{blockIndex})
last = keyBlock.Sum(nil)
blockIndex += 1
out = append(out, last...)
}
return out[:length], nil
} else { } else {
h := hkdf.New(sha256.New, key, nil, []byte(info)) /*
out := make([]byte, length) Used every other time
n, err := io.ReadAtLeast(h, out, length) Pseudorandom Key is created during kdf.New
if err != nil { This is the normal that crypto/hkdf is used
return nil, err */
} h = hkdf.New(sha256.New, key, nil, []byte(info))
if n != length {
return nil, fmt.Errorf("new key to short")
}
return out[:length], nil
} }
out := make([]byte, length)
n, err := io.ReadAtLeast(h, out, length)
if err != nil {
return nil, err
}
if n != length {
return nil, fmt.Errorf("new key to short")
}
return out, nil
} }

35
vendor/github.com/Rhymen/go-whatsapp/errors.go generated vendored Normal file
View File

@@ -0,0 +1,35 @@
package whatsapp
import (
"fmt"
"github.com/pkg/errors"
)
var (
ErrAlreadyConnected = errors.New("already connected")
ErrAlreadyLoggedIn = errors.New("already logged in")
ErrInvalidSession = errors.New("invalid session")
ErrLoginInProgress = errors.New("login or restore already running")
ErrNotConnected = errors.New("not connected")
ErrInvalidWsData = errors.New("received invalid data")
ErrConnectionTimeout = errors.New("connection timed out")
ErrMissingMessageTag = errors.New("no messageTag specified or to short")
ErrInvalidHmac = errors.New("invalid hmac")
)
type ErrConnectionFailed struct {
Err error
}
func (e *ErrConnectionFailed) Error() string {
return fmt.Sprintf("connection to WhatsApp servers failed: %v", e.Err)
}
type ErrConnectionClosed struct {
Code int
Text string
}
func (e *ErrConnectionClosed) Error() string {
return fmt.Sprintf("server closed connection,code: %d,text: %s", e.Code, e.Text)
}

View File

@@ -1,8 +1,12 @@
module github.com/Rhymen/go-whatsapp module github.com/Rhymen/go-whatsapp
require ( require (
github.com/golang/protobuf v1.2.0 github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/golang/protobuf v1.3.0
github.com/gorilla/websocket v1.4.0 github.com/gorilla/websocket v1.4.0
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 github.com/pkg/errors v0.8.1
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
) )

View File

@@ -1,4 +1,35 @@
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/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA=
github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d h1:m3wkrunHupL9XzzM+JZu1pgoDV1d9LFtD0gedNTHVDU=
github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME=
github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d h1:muQlzqfZxjptOBjPdv+UoxVMr8Y1rPx7VMGPJIAFc5w=
github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU=
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d h1:xP//3V77YvHd1cj2Z3ttuQWAvs5WmIwBbjKe/t0g/tM=
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw=
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d h1:IRmRE0SPMByczwE2dhnTcVojje3w2TCSKwFrboLUbDg=
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
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/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
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.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

90
vendor/github.com/Rhymen/go-whatsapp/group.go generated vendored Normal file
View File

@@ -0,0 +1,90 @@
package whatsapp
import (
"encoding/json"
"fmt"
"time"
)
func (wac *Conn) GetGroupMetaData(jid string) (<-chan string, error) {
data := []interface{}{"query", "GroupMetadata", jid}
return wac.writeJson(data)
}
func (wac *Conn) CreateGroup(subject string, participants []string) (<-chan string, error) {
return wac.setGroup("create", "", subject, participants)
}
func (wac *Conn) UpdateGroupSubject(subject string, jid string) (<-chan string, error) {
return wac.setGroup("subject", jid, subject, nil)
}
func (wac *Conn) SetAdmin(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("promote", jid, "", participants)
}
func (wac *Conn) RemoveAdmin(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("demote", jid, "", participants)
}
func (wac *Conn) AddMember(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("add", jid, "", participants)
}
func (wac *Conn) RemoveMember(jid string, participants []string) (<-chan string, error) {
return wac.setGroup("remove", jid, "", participants)
}
func (wac *Conn) LeaveGroup(jid string) (<-chan string, error) {
return wac.setGroup("leave", jid, "", nil)
}
func (wac *Conn) GroupInviteLink(jid string) (string, error) {
request := []interface{}{"query", "inviteCode", jid}
ch, err := wac.writeJson(request)
if err != nil {
return "", err
}
var response map[string]interface{}
select {
case r := <-ch:
if err := json.Unmarshal([]byte(r), &response); err != nil {
return "", fmt.Errorf("error decoding response message: %v\n", err)
}
case <-time.After(wac.msgTimeout):
return "", fmt.Errorf("request timed out")
}
if int(response["status"].(float64)) != 200 {
return "", fmt.Errorf("request responded with %d", response["status"])
}
return response["code"].(string), nil
}
func (wac *Conn) GroupAcceptInviteCode(code string) (jid string, err error) {
request := []interface{}{"action", "invite", code}
ch, err := wac.writeJson(request)
if err != nil {
return "", err
}
var response map[string]interface{}
select {
case r := <-ch:
if err := json.Unmarshal([]byte(r), &response); err != nil {
return "", fmt.Errorf("error decoding response message: %v\n", err)
}
case <-time.After(wac.msgTimeout):
return "", fmt.Errorf("request timed out")
}
if int(response["status"].(float64)) != 200 {
return "", fmt.Errorf("request responded with %d", response["status"])
}
return response["gid"].(string), nil
}

View File

@@ -2,9 +2,11 @@ package whatsapp
import ( import (
"fmt" "fmt"
"os"
"strings"
"github.com/Rhymen/go-whatsapp/binary" "github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/binary/proto" "github.com/Rhymen/go-whatsapp/binary/proto"
"os"
) )
/* /*
@@ -78,6 +80,22 @@ type RawMessageHandler interface {
HandleRawMessage(message *proto.WebMessageInfo) HandleRawMessage(message *proto.WebMessageInfo)
} }
/**
The ContactListHandler interface needs to be implemented to applky custom actions to contact lists dispatched by the dispatcher.
*/
type ContactListHandler interface {
Handler
HandleContactList(contacts []Contact)
}
/**
The ChatListHandler interface needs to be implemented to apply custom actions to chat lists dispatched by the dispatcher.
*/
type ChatListHandler interface {
Handler
HandleChatList(contacts []Chat)
}
/* /*
AddHandler adds an handler to the list of handler that receive dispatched messages. AddHandler adds an handler to the list of handler that receive dispatched messages.
The provided handler must at least implement the Handler interface. Additionally implemented The provided handler must at least implement the Handler interface. Additionally implemented
@@ -88,6 +106,27 @@ func (wac *Conn) AddHandler(handler Handler) {
wac.handler = append(wac.handler, handler) wac.handler = append(wac.handler, handler)
} }
// RemoveHandler removes a handler from the list of handlers that receive dispatched messages.
func (wac *Conn) RemoveHandler(handler Handler) bool {
i := -1
for k, v := range wac.handler {
if v == handler {
i = k
break
}
}
if i > -1 {
wac.handler = append(wac.handler[:i], wac.handler[i+1:]...)
return true
}
return false
}
// RemoveHandlers empties the list of handlers that receive dispatched messages.
func (wac *Conn) RemoveHandlers() {
wac.handler = make([]Handler, 0)
}
func (wac *Conn) handle(message interface{}) { func (wac *Conn) handle(message interface{}) {
switch m := message.(type) { switch m := message.(type) {
case error: case error:
@@ -140,6 +179,62 @@ func (wac *Conn) handle(message interface{}) {
} }
func (wac *Conn) handleContacts(contacts interface{}) {
var contactList []Contact
c, ok := contacts.([]interface{})
if !ok {
return
}
for _, contact := range c {
contactNode, ok := contact.(binary.Node)
if !ok {
continue
}
jid := strings.Replace(contactNode.Attributes["jid"], "@c.us", "@s.whatsapp.net", 1)
contactList = append(contactList, Contact{
jid,
contactNode.Attributes["notify"],
contactNode.Attributes["name"],
contactNode.Attributes["short"],
})
}
for _, h := range wac.handler {
if x, ok := h.(ContactListHandler); ok {
go x.HandleContactList(contactList)
}
}
}
func (wac *Conn) handleChats(chats interface{}) {
var chatList []Chat
c, ok := chats.([]interface{})
if !ok {
return
}
for _, chat := range c {
chatNode, ok := chat.(binary.Node)
if !ok {
continue
}
jid := strings.Replace(chatNode.Attributes["jid"], "@c.us", "@s.whatsapp.net", 1)
chatList = append(chatList, Chat{
jid,
chatNode.Attributes["name"],
chatNode.Attributes["count"],
chatNode.Attributes["t"],
chatNode.Attributes["mute"],
chatNode.Attributes["spam"],
})
}
for _, h := range wac.handler {
if x, ok := h.(ChatListHandler); ok {
go x.HandleChatList(chatList)
}
}
}
func (wac *Conn) dispatch(msg interface{}) { func (wac *Conn) dispatch(msg interface{}) {
if msg == nil { if msg == nil {
return return
@@ -158,6 +253,10 @@ func (wac *Conn) dispatch(msg interface{}) {
} }
} else if message.Description == "response" && message.Attributes["type"] == "contacts" { } else if message.Description == "response" && message.Attributes["type"] == "contacts" {
wac.updateContacts(message.Content) wac.updateContacts(message.Content)
wac.handleContacts(message.Content)
} else if message.Description == "response" && message.Attributes["type"] == "chat" {
wac.updateChats(message.Content)
wac.handleChats(message.Content)
} }
case error: case error:
wac.handle(message) wac.handle(message)

View File

@@ -133,7 +133,7 @@ func (wac *Conn) Upload(reader io.Reader, appInfo MediaType) (url string, mediaK
} }
uploadReq := []interface{}{"action", "encr_upload", filetype, base64.StdEncoding.EncodeToString(fileEncSha256)} uploadReq := []interface{}{"action", "encr_upload", filetype, base64.StdEncoding.EncodeToString(fileEncSha256)}
ch, err := wac.write(uploadReq) ch, err := wac.writeJson(uploadReq)
if err != nil { if err != nil {
return "", nil, nil, nil, 0, err return "", nil, nil, nil, 0, err
} }

View File

@@ -22,61 +22,77 @@ const (
MediaDocument MediaType = "WhatsApp Document Keys" MediaDocument MediaType = "WhatsApp Document Keys"
) )
func (wac *Conn) Send(msg interface{}) error { var msgInfo MessageInfo
func (wac *Conn) Send(msg interface{}) (string, error) {
var err error var err error
var ch <-chan string var ch <-chan string
var msgProto *proto.WebMessageInfo
switch m := msg.(type) { switch m := msg.(type) {
case *proto.WebMessageInfo: case *proto.WebMessageInfo:
ch, err = wac.sendProto(m) ch, err = wac.sendProto(m)
case TextMessage: case TextMessage:
ch, err = wac.sendProto(getTextProto(m)) msgProto = getTextProto(m)
msgInfo = getMessageInfo(msgProto)
ch, err = wac.sendProto(msgProto)
case ImageMessage: case ImageMessage:
m.url, m.mediaKey, m.fileEncSha256, m.fileSha256, m.fileLength, err = wac.Upload(m.Content, MediaImage) m.url, m.mediaKey, m.fileEncSha256, m.fileSha256, m.fileLength, err = wac.Upload(m.Content, MediaImage)
if err != nil { if err != nil {
return fmt.Errorf("image upload failed: %v", err) return "ERROR", fmt.Errorf("image upload failed: %v", err)
} }
ch, err = wac.sendProto(getImageProto(m)) msgProto = getImageProto(m)
msgInfo = getMessageInfo(msgProto)
ch, err = wac.sendProto(msgProto)
case VideoMessage: case VideoMessage:
m.url, m.mediaKey, m.fileEncSha256, m.fileSha256, m.fileLength, err = wac.Upload(m.Content, MediaVideo) m.url, m.mediaKey, m.fileEncSha256, m.fileSha256, m.fileLength, err = wac.Upload(m.Content, MediaVideo)
if err != nil { if err != nil {
return fmt.Errorf("video upload failed: %v", err) return "ERROR", fmt.Errorf("video upload failed: %v", err)
} }
ch, err = wac.sendProto(getVideoProto(m)) msgProto = getVideoProto(m)
msgInfo = getMessageInfo(msgProto)
ch, err = wac.sendProto(msgProto)
case DocumentMessage: case DocumentMessage:
m.url, m.mediaKey, m.fileEncSha256, m.fileSha256, m.fileLength, err = wac.Upload(m.Content, MediaDocument) m.url, m.mediaKey, m.fileEncSha256, m.fileSha256, m.fileLength, err = wac.Upload(m.Content, MediaDocument)
if err != nil { if err != nil {
return fmt.Errorf("document upload failed: %v", err) return "ERROR", fmt.Errorf("document upload failed: %v", err)
} }
ch, err = wac.sendProto(getDocumentProto(m)) msgProto = getDocumentProto(m)
msgInfo = getMessageInfo(msgProto)
ch, err = wac.sendProto(msgProto)
case AudioMessage: case AudioMessage:
m.url, m.mediaKey, m.fileEncSha256, m.fileSha256, m.fileLength, err = wac.Upload(m.Content, MediaAudio) m.url, m.mediaKey, m.fileEncSha256, m.fileSha256, m.fileLength, err = wac.Upload(m.Content, MediaAudio)
if err != nil { if err != nil {
return fmt.Errorf("audio upload failed: %v", err) return "ERROR", fmt.Errorf("audio upload failed: %v", err)
} }
ch, err = wac.sendProto(getAudioProto(m)) msgProto = getAudioProto(m)
msgInfo = getMessageInfo(msgProto)
ch, err = wac.sendProto(msgProto)
default: default:
return fmt.Errorf("cannot match type %T, use message types declared in the package", msg) return "ERROR", fmt.Errorf("cannot match type %T, use message types declared in the package", msg)
} }
if err != nil { if err != nil {
return fmt.Errorf("could not send proto: %v", err) return "ERROR", fmt.Errorf("could not send proto: %v", err)
} }
select { select {
case response := <-ch: case response := <-ch:
var resp map[string]interface{} var resp map[string]interface{}
if err = json.Unmarshal([]byte(response), &resp); err != nil { if err = json.Unmarshal([]byte(response), &resp); err != nil {
return fmt.Errorf("error decoding sending response: %v\n", err) return "ERROR", fmt.Errorf("error decoding sending response: %v\n", err)
} }
if int(resp["status"].(float64)) != 200 { if int(resp["status"].(float64)) != 200 {
return fmt.Errorf("message sending responded with %d", resp["status"]) return "ERROR", fmt.Errorf("message sending responded with %d", resp["status"])
}
if int(resp["status"].(float64)) == 200 {
return msgInfo.Id, nil
} }
case <-time.After(wac.msgTimeout): case <-time.After(wac.msgTimeout):
return fmt.Errorf("sending message timed out") return "ERROR", fmt.Errorf("sending message timed out")
} }
return nil return "ERROR", nil
} }
func (wac *Conn) sendProto(p *proto.WebMessageInfo) (<-chan string, error) { func (wac *Conn) sendProto(p *proto.WebMessageInfo) (<-chan string, error) {
@@ -363,6 +379,7 @@ type DocumentMessage struct {
Title string Title string
PageCount uint32 PageCount uint32
Type string Type string
FileName string
Thumbnail []byte Thumbnail []byte
Content io.Reader Content io.Reader
url string url string
@@ -376,15 +393,16 @@ func getDocumentMessage(msg *proto.WebMessageInfo) DocumentMessage {
doc := msg.GetMessage().GetDocumentMessage() doc := msg.GetMessage().GetDocumentMessage()
return DocumentMessage{ return DocumentMessage{
Info: getMessageInfo(msg), Info: getMessageInfo(msg),
Title: doc.GetTitle(),
PageCount: doc.GetPageCount(),
Type: doc.GetMimetype(),
FileName: doc.GetFileName(),
Thumbnail: doc.GetJpegThumbnail(), Thumbnail: doc.GetJpegThumbnail(),
url: doc.GetUrl(), url: doc.GetUrl(),
mediaKey: doc.GetMediaKey(), mediaKey: doc.GetMediaKey(),
fileEncSha256: doc.GetFileEncSha256(), fileEncSha256: doc.GetFileEncSha256(),
fileSha256: doc.GetFileSha256(), fileSha256: doc.GetFileSha256(),
fileLength: doc.GetFileLength(), fileLength: doc.GetFileLength(),
PageCount: doc.GetPageCount(),
Title: doc.GetTitle(),
Type: doc.GetMimetype(),
} }
} }

111
vendor/github.com/Rhymen/go-whatsapp/read.go generated vendored Normal file
View File

@@ -0,0 +1,111 @@
package whatsapp
import (
"crypto/hmac"
"crypto/sha256"
"github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/crypto/cbc"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"io"
"io/ioutil"
"strings"
)
func (wac *Conn) readPump() {
defer wac.wg.Done()
var readErr error
var msgType int
var reader io.Reader
for {
readerFound := make(chan struct{})
go func() {
msgType, reader, readErr = wac.ws.conn.NextReader()
close(readerFound)
}()
select {
case <-readerFound:
if readErr != nil {
wac.handle(&ErrConnectionFailed{Err: readErr})
_, _ = wac.Disconnect()
return
}
msg, err := ioutil.ReadAll(reader)
if err != nil {
wac.handle(errors.Wrap(err, "error reading message from Reader"))
continue
}
err = wac.processReadData(msgType, msg)
if err != nil {
wac.handle(errors.Wrap(err, "error processing data"))
}
case <-wac.ws.close:
return
}
}
}
func (wac *Conn) processReadData(msgType int, msg []byte) error {
data := strings.SplitN(string(msg), ",", 2)
if data[0][0] == '!' { //Keep-Alive Timestamp
data = append(data, data[0][1:]) //data[1]
data[0] = "!"
}
if len(data) != 2 || len(data[1]) == 0 {
return ErrInvalidWsData
}
wac.listener.RLock()
listener, hasListener := wac.listener.m[data[0]]
wac.listener.RUnlock()
if hasListener {
// listener only exists for TextMessages query messages out of contact.go
// If these binary query messages can be handled another way,
// then the TextMessages, which are all JSON encoded, can directly
// be unmarshalled. The listener chan could then be changed from type
// chan string to something like chan map[string]interface{}. The unmarshalling
// in several places, especially in session.go, would then be gone.
listener <- data[1]
wac.listener.Lock()
delete(wac.listener.m, data[0])
wac.listener.Unlock()
} else if msgType == websocket.BinaryMessage && wac.loggedIn {
message, err := wac.decryptBinaryMessage([]byte(data[1]))
if err != nil {
return errors.Wrap(err, "error decoding binary")
}
wac.dispatch(message)
} else { //RAW json status updates
wac.handle(string(data[1]))
}
return nil
}
func (wac *Conn) decryptBinaryMessage(msg []byte) (*binary.Node, error) {
//message validation
h2 := hmac.New(sha256.New, wac.session.MacKey)
h2.Write([]byte(msg[32:]))
if !hmac.Equal(h2.Sum(nil), msg[:32]) {
return nil, ErrInvalidHmac
}
// message decrypt
d, err := cbc.Decrypt(wac.session.EncKey, nil, msg[32:])
if err != nil {
return nil, errors.Wrap(err, "decrypting message with AES-CBC failed")
}
// message unmarshal
message, err := binary.Unmarshal(d)
if err != nil {
return nil, errors.Wrap(err, "could not decode binary")
}
return message, nil
}

View File

@@ -7,6 +7,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"sync/atomic"
"time" "time"
"github.com/Rhymen/go-whatsapp/crypto/cbc" "github.com/Rhymen/go-whatsapp/crypto/cbc"
@@ -14,9 +15,12 @@ import (
"github.com/Rhymen/go-whatsapp/crypto/hkdf" "github.com/Rhymen/go-whatsapp/crypto/hkdf"
) )
//represents the WhatsAppWeb client version
var waVersion = []int{0, 3, 3324}
/* /*
Session contains session individual information. To be able to resume the connection without scanning the qr code Session contains session individual information. To be able to resume the connection without scanning the qr code
every time you should save the Session returned by Login and use RestoreSession the next time you want to login. every time you should save the Session returned by Login and use RestoreWithSession the next time you want to login.
Every successful created connection returns a new Session. The Session(ClientToken, ServerToken) is altered after Every successful created connection returns a new Session. The Session(ClientToken, ServerToken) is altered after
every re-login and should be saved every time. every re-login and should be saved every time.
*/ */
@@ -98,7 +102,7 @@ func (wac *Conn) SetClientName(long, short string) error {
/* /*
Login is the function that creates a new whatsapp session and logs you in. If you do not want to scan the qr code Login is the function that creates a new whatsapp session and logs you in. If you do not want to scan the qr code
every time, you should save the returned session and use RestoreSession the next time. Login takes a writable channel every time, you should save the returned session and use RestoreWithSession the next time. Login takes a writable channel
as an parameter. This channel is used to push the data represented by the qr code back to the user. The received data as an parameter. This channel is used to push the data represented by the qr code back to the user. The received data
should be displayed as an qr code in a way you prefer. To print a qr code to console you can use: should be displayed as an qr code in a way you prefer. To print a qr code to console you can use:
github.com/Baozisoftware/qrcode-terminal-go Example login procedure: github.com/Baozisoftware/qrcode-terminal-go Example login procedure:
@@ -121,7 +125,21 @@ github.com/Baozisoftware/qrcode-terminal-go Example login procedure:
*/ */
func (wac *Conn) Login(qrChan chan<- string) (Session, error) { func (wac *Conn) Login(qrChan chan<- string) (Session, error) {
session := Session{} session := Session{}
//Makes sure that only a single Login or Restore can happen at the same time
if !atomic.CompareAndSwapUint32(&wac.sessionLock, 0, 1) {
return session, ErrLoginInProgress
}
defer atomic.StoreUint32(&wac.sessionLock, 0)
if wac.loggedIn {
return session, ErrAlreadyLoggedIn
}
if err := wac.connect(); err != nil && err != ErrAlreadyConnected {
return session, err
}
//logged in?!?
if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) { if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) {
return session, fmt.Errorf("already logged in") return session, fmt.Errorf("already logged in")
} }
@@ -133,9 +151,8 @@ func (wac *Conn) Login(qrChan chan<- string) (Session, error) {
} }
session.ClientId = base64.StdEncoding.EncodeToString(clientId) session.ClientId = base64.StdEncoding.EncodeToString(clientId)
//oldVersion=8691 login := []interface{}{"admin", "init", waVersion, []string{wac.longClientName, wac.shortClientName}, session.ClientId, true}
login := []interface{}{"admin", "init", []int{0, 3, 225}, []string{wac.longClientName, wac.shortClientName}, session.ClientId, true} loginChan, err := wac.writeJson(login)
loginChan, err := wac.write(login)
if err != nil { if err != nil {
return session, fmt.Errorf("error writing login: %v\n", err) return session, fmt.Errorf("error writing login: %v\n", err)
} }
@@ -160,14 +177,16 @@ func (wac *Conn) Login(qrChan chan<- string) (Session, error) {
} }
//listener for Login response //listener for Login response
messageTag := "s1" s1 := make(chan string, 1)
wac.listener[messageTag] = make(chan string, 1) wac.listener.Lock()
wac.listener.m["s1"] = s1
wac.listener.Unlock()
qrChan <- fmt.Sprintf("%v,%v,%v", ref, base64.StdEncoding.EncodeToString(pub[:]), session.ClientId) qrChan <- fmt.Sprintf("%v,%v,%v", ref, base64.StdEncoding.EncodeToString(pub[:]), session.ClientId)
var resp2 []interface{} var resp2 []interface{}
select { select {
case r1 := <-wac.listener[messageTag]: case r1 := <-s1:
if err := json.Unmarshal([]byte(r1), &resp2); err != nil { if err := json.Unmarshal([]byte(r1), &resp2); err != nil {
return session, fmt.Errorf("error decoding qr code resp: %v", err) return session, fmt.Errorf("error decoding qr code resp: %v", err)
} }
@@ -226,90 +245,136 @@ func (wac *Conn) Login(qrChan chan<- string) (Session, error) {
session.EncKey = keyDecrypted[:32] session.EncKey = keyDecrypted[:32]
session.MacKey = keyDecrypted[32:64] session.MacKey = keyDecrypted[32:64]
wac.session = &session wac.session = &session
wac.loggedIn = true
return session, nil return session, nil
} }
//TODO: GoDoc
/* /*
RestoreSession is the function that restores a given session. It will try to reestablish the connection to the Basically the old RestoreSession functionality
*/
func (wac *Conn) RestoreWithSession(session Session) (_ Session, err error) {
if wac.loggedIn {
return Session{}, ErrAlreadyLoggedIn
}
old := wac.session
defer func() {
if err != nil {
wac.session = old
}
}()
wac.session = &session
if err = wac.Restore(); err != nil {
wac.session = nil
return Session{}, err
}
return *wac.session, nil
}
/*//TODO: GoDoc
RestoreWithSession is the function that restores a given session. It will try to reestablish the connection to the
WhatsAppWeb servers with the provided session. If it succeeds it will return a new session. This new session has to be WhatsAppWeb servers with the provided session. If it succeeds it will return a new session. This new session has to be
saved because the Client and Server-Token will change after every login. Logging in with old tokens is possible, but not saved because the Client and Server-Token will change after every login. Logging in with old tokens is possible, but not
suggested. If so, a challenge has to be resolved which is just another possible point of failure. suggested. If so, a challenge has to be resolved which is just another possible point of failure.
*/ */
func (wac *Conn) RestoreSession(session Session) (Session, error) { func (wac *Conn) Restore() error {
if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) { //Makes sure that only a single Login or Restore can happen at the same time
return Session{}, fmt.Errorf("already logged in") if !atomic.CompareAndSwapUint32(&wac.sessionLock, 0, 1) {
return ErrLoginInProgress
}
defer atomic.StoreUint32(&wac.sessionLock, 0)
if wac.session == nil {
return ErrInvalidSession
} }
wac.session = &session if err := wac.connect(); err != nil && err != ErrAlreadyConnected {
return err
}
if wac.loggedIn {
return ErrAlreadyLoggedIn
}
//listener for Conn or challenge; s1 is not allowed to drop //listener for Conn or challenge; s1 is not allowed to drop
wac.listener["s1"] = make(chan string, 1) s1 := make(chan string, 1)
wac.listener.Lock()
wac.listener.m["s1"] = s1
wac.listener.Unlock()
//admin init //admin init
init := []interface{}{"admin", "init", []int{0, 3, 225}, []string{wac.longClientName, wac.shortClientName}, session.ClientId, true} init := []interface{}{"admin", "init", waVersion, []string{wac.longClientName, wac.shortClientName}, wac.session.ClientId, true}
initChan, err := wac.write(init) initChan, err := wac.writeJson(init)
if err != nil { if err != nil {
wac.session = nil return fmt.Errorf("error writing admin init: %v\n", err)
return Session{}, fmt.Errorf("error writing admin init: %v\n", err)
} }
//admin login with takeover //admin login with takeover
login := []interface{}{"admin", "login", session.ClientToken, session.ServerToken, session.ClientId, "takeover"} login := []interface{}{"admin", "login", wac.session.ClientToken, wac.session.ServerToken, wac.session.ClientId, "takeover"}
loginChan, err := wac.write(login) loginChan, err := wac.writeJson(login)
if err != nil { if err != nil {
wac.session = nil return fmt.Errorf("error writing admin login: %v\n", err)
return Session{}, fmt.Errorf("error writing admin login: %v\n", err)
} }
select { select {
case r := <-initChan: case r := <-initChan:
var resp map[string]interface{} var resp map[string]interface{}
if err = json.Unmarshal([]byte(r), &resp); err != nil { if err = json.Unmarshal([]byte(r), &resp); err != nil {
wac.session = nil return fmt.Errorf("error decoding login connResp: %v\n", err)
return Session{}, fmt.Errorf("error decoding login connResp: %v\n", err)
} }
if int(resp["status"].(float64)) != 200 { if int(resp["status"].(float64)) != 200 {
wac.session = nil return fmt.Errorf("init responded with %d", resp["status"])
return Session{}, fmt.Errorf("init responded with %d", resp["status"])
} }
case <-time.After(wac.msgTimeout): case <-time.After(wac.msgTimeout):
wac.session = nil return fmt.Errorf("restore session init timed out")
return Session{}, fmt.Errorf("restore session init timed out")
} }
//wait for s1 //wait for s1
var connResp []interface{} var connResp []interface{}
select { select {
case r1 := <-wac.listener["s1"]: case r1 := <-s1:
if err := json.Unmarshal([]byte(r1), &connResp); err != nil { if err := json.Unmarshal([]byte(r1), &connResp); err != nil {
wac.session = nil return fmt.Errorf("error decoding s1 message: %v\n", err)
return Session{}, fmt.Errorf("error decoding s1 message: %v\n", err)
} }
case <-time.After(wac.msgTimeout): case <-time.After(wac.msgTimeout):
wac.session = nil
return Session{}, fmt.Errorf("restore session connection timed out") //check for an error message
select {
case r := <-loginChan:
var resp map[string]interface{}
if err = json.Unmarshal([]byte(r), &resp); err != nil {
return fmt.Errorf("error decoding login connResp: %v\n", err)
}
if int(resp["status"].(float64)) != 200 {
return fmt.Errorf("admin login responded with %d", int(resp["status"].(float64)))
}
default:
// not even an error message assume timeout
return fmt.Errorf("restore session connection timed out")
}
} }
//check if challenge is present //check if challenge is present
if len(connResp) == 2 && connResp[0] == "Cmd" && connResp[1].(map[string]interface{})["type"] == "challenge" { if len(connResp) == 2 && connResp[0] == "Cmd" && connResp[1].(map[string]interface{})["type"] == "challenge" {
wac.listener["s2"] = make(chan string, 1) s2 := make(chan string, 1)
wac.listener.Lock()
wac.listener.m["s2"] = s2
wac.listener.Unlock()
if err := wac.resolveChallenge(connResp[1].(map[string]interface{})["challenge"].(string)); err != nil { if err := wac.resolveChallenge(connResp[1].(map[string]interface{})["challenge"].(string)); err != nil {
wac.session = nil return fmt.Errorf("error resolving challenge: %v\n", err)
return Session{}, fmt.Errorf("error resolving challenge: %v\n", err)
} }
select { select {
case r := <-wac.listener["s2"]: case r := <-s2:
if err := json.Unmarshal([]byte(r), &connResp); err != nil { if err := json.Unmarshal([]byte(r), &connResp); err != nil {
wac.session = nil return fmt.Errorf("error decoding s2 message: %v\n", err)
return Session{}, fmt.Errorf("error decoding s2 message: %v\n", err)
} }
case <-time.After(wac.msgTimeout): case <-time.After(wac.msgTimeout):
wac.session = nil return fmt.Errorf("restore session challenge timed out")
return Session{}, fmt.Errorf("restore session challenge timed out")
} }
} }
@@ -318,17 +383,14 @@ func (wac *Conn) RestoreSession(session Session) (Session, error) {
case r := <-loginChan: case r := <-loginChan:
var resp map[string]interface{} var resp map[string]interface{}
if err = json.Unmarshal([]byte(r), &resp); err != nil { if err = json.Unmarshal([]byte(r), &resp); err != nil {
wac.session = nil return fmt.Errorf("error decoding login connResp: %v\n", err)
return Session{}, fmt.Errorf("error decoding login connResp: %v\n", err)
} }
if int(resp["status"].(float64)) != 200 { if int(resp["status"].(float64)) != 200 {
wac.session = nil return fmt.Errorf("admin login responded with %d", resp["status"])
return Session{}, fmt.Errorf("admin login responded with %d", resp["status"])
} }
case <-time.After(wac.msgTimeout): case <-time.After(wac.msgTimeout):
wac.session = nil return fmt.Errorf("restore session login timed out")
return Session{}, fmt.Errorf("restore session login timed out")
} }
info := connResp[1].(map[string]interface{}) info := connResp[1].(map[string]interface{})
@@ -336,11 +398,12 @@ func (wac *Conn) RestoreSession(session Session) (Session, error) {
wac.Info = newInfoFromReq(info) wac.Info = newInfoFromReq(info)
//set new tokens //set new tokens
session.ClientToken = info["clientToken"].(string) wac.session.ClientToken = info["clientToken"].(string)
session.ServerToken = info["serverToken"].(string) wac.session.ServerToken = info["serverToken"].(string)
session.Wid = info["wid"].(string) wac.session.Wid = info["wid"].(string)
wac.loggedIn = true
return *wac.session, nil return nil
} }
func (wac *Conn) resolveChallenge(challenge string) error { func (wac *Conn) resolveChallenge(challenge string) error {
@@ -353,7 +416,7 @@ func (wac *Conn) resolveChallenge(challenge string) error {
h2.Write([]byte(decoded)) h2.Write([]byte(decoded))
ch := []interface{}{"admin", "challenge", base64.StdEncoding.EncodeToString(h2.Sum(nil)), wac.session.ServerToken, wac.session.ClientId} ch := []interface{}{"admin", "challenge", base64.StdEncoding.EncodeToString(h2.Sum(nil)), wac.session.ServerToken, wac.session.ClientId}
challengeChan, err := wac.write(ch) challengeChan, err := wac.writeJson(ch)
if err != nil { if err != nil {
return fmt.Errorf("error writing challenge: %v\n", err) return fmt.Errorf("error writing challenge: %v\n", err)
} }
@@ -380,7 +443,7 @@ The session can not be resumed and will disappear on your phone in the WhatsAppW
*/ */
func (wac *Conn) Logout() error { func (wac *Conn) Logout() error {
login := []interface{}{"admin", "Conn", "disconnect"} login := []interface{}{"admin", "Conn", "disconnect"}
_, err := wac.write(login) _, err := wac.writeJson(login)
if err != nil { if err != nil {
return fmt.Errorf("error writing logout: %v\n", err) return fmt.Errorf("error writing logout: %v\n", err)
} }

View File

@@ -7,6 +7,7 @@ import (
type Store struct { type Store struct {
Contacts map[string]Contact Contacts map[string]Contact
Chats map[string]Chat
} }
type Contact struct { type Contact struct {
@@ -16,9 +17,19 @@ type Contact struct {
Short string Short string
} }
type Chat struct {
Jid string
Name string
Unread string
LastMessageTime string
IsMuted string
IsMarkedSpam string
}
func newStore() *Store { func newStore() *Store {
return &Store{ return &Store{
make(map[string]Contact), make(map[string]Contact),
make(map[string]Chat),
} }
} }
@@ -43,3 +54,27 @@ func (wac *Conn) updateContacts(contacts interface{}) {
} }
} }
} }
func (wac *Conn) updateChats(chats interface{}) {
c, ok := chats.([]interface{})
if !ok {
return
}
for _, chat := range c {
chatNode, ok := chat.(binary.Node)
if !ok {
continue
}
jid := strings.Replace(chatNode.Attributes["jid"], "@c.us", "@s.whatsapp.net", 1)
wac.Store.Chats[jid] = Chat{
jid,
chatNode.Attributes["name"],
chatNode.Attributes["count"],
chatNode.Attributes["t"],
chatNode.Attributes["mute"],
chatNode.Attributes["spam"],
}
}
}

125
vendor/github.com/Rhymen/go-whatsapp/write.go generated vendored Normal file
View File

@@ -0,0 +1,125 @@
package whatsapp
import (
"crypto/hmac"
"crypto/sha256"
"encoding/json"
"fmt"
"github.com/Rhymen/go-whatsapp/binary"
"github.com/Rhymen/go-whatsapp/crypto/cbc"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"strconv"
"time"
)
//writeJson enqueues a json message into the writeChan
func (wac *Conn) writeJson(data []interface{}) (<-chan string, error) {
d, err := json.Marshal(data)
if err != nil {
return nil, err
}
ts := time.Now().Unix()
messageTag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
bytes := []byte(fmt.Sprintf("%s,%s", messageTag, d))
ch, err := wac.write(websocket.TextMessage, messageTag, bytes)
if err != nil {
return nil, err
}
wac.msgCount++
return ch, nil
}
func (wac *Conn) writeBinary(node binary.Node, metric metric, flag flag, messageTag string) (<-chan string, error) {
if len(messageTag) < 2 {
return nil, ErrMissingMessageTag
}
data, err := wac.encryptBinaryMessage(node)
if err != nil {
return nil, errors.Wrap(err, "encryptBinaryMessage(node) failed")
}
bytes := []byte(messageTag + ",")
bytes = append(bytes, byte(metric), byte(flag))
bytes = append(bytes, data...)
ch, err := wac.write(websocket.BinaryMessage, messageTag, bytes)
if err != nil {
return nil, errors.Wrap(err, "failed to write message")
}
wac.msgCount++
return ch, nil
}
func (wac *Conn) sendKeepAlive() error {
bytes := []byte("?,,")
respChan, err := wac.write(websocket.TextMessage, "!", bytes)
if err != nil {
return errors.Wrap(err, "error sending keepAlive")
}
select {
case resp := <-respChan:
msecs, err := strconv.ParseInt(resp, 10, 64)
if err != nil {
return errors.Wrap(err, "Error converting time string to uint")
}
wac.ServerLastSeen = time.Unix(msecs/1000, (msecs%1000)*int64(time.Millisecond))
case <-time.After(wac.msgTimeout):
return ErrConnectionTimeout
}
return nil
}
func (wac *Conn) write(messageType int, answerMessageTag string, data []byte) (<-chan string, error) {
var ch chan string
if answerMessageTag != "" {
ch = make(chan string, 1)
wac.listener.Lock()
wac.listener.m[answerMessageTag] = ch
wac.listener.Unlock()
}
wac.ws.Lock()
err := wac.ws.conn.WriteMessage(messageType, data)
wac.ws.Unlock()
if err != nil {
if answerMessageTag != "" {
wac.listener.Lock()
delete(wac.listener.m, answerMessageTag)
wac.listener.Unlock()
}
return nil, errors.Wrap(err, "error writing to websocket")
}
return ch, nil
}
func (wac *Conn) encryptBinaryMessage(node binary.Node) (data []byte, err error) {
b, err := binary.Marshal(node)
if err != nil {
return nil, errors.Wrap(err, "binary node marshal failed")
}
cipher, err := cbc.Encrypt(wac.session.EncKey, nil, b)
if err != nil {
return nil, errors.Wrap(err, "encrypt failed")
}
h := hmac.New(sha256.New, wac.session.MacKey)
h.Write(cipher)
hash := h.Sum(nil)
data = append(data, hash[:32]...)
data = append(data, cipher...)
return data, 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

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

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

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

@@ -0,0 +1,77 @@
<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)
[![Sourcegraph](https://sourcegraph.com/github.com/d5/tengo/-/badge.svg)](https://sourcegraph.com/github.com/d5/tengo?badge)
**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 */
fmt := import("fmt")
each := func(seq, fn) {
for x in seq { fn(x) }
}
sum := func(init, seq) {
each(seq, func(x) { init += x })
return init
}
fmt.println(sum(0, [1, 2, 3])) // "6"
fmt.println(sum("", [1, 2, 3])) // "123"
```
> Run this code in the [Playground](https://tengolang.com/?s=0c8d5d0d88f2795a7093d7f35ae12c3afa17bea3)
## 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
- Use cases: rules engine, [state machine](https://github.com/d5/go-fsm), [gaming](https://github.com/d5/pbr), data pipeline, [transpiler](https://github.com/d5/tengo2lua)
## Benchmark
| | fib(35) | fibt(35) | Type |
| :--- | ---: | ---: | :---: |
| Go | `48ms` | `3ms` | Go (native) |
| [**Tengo**](https://github.com/d5/tengo) | `2,349ms` | `5ms` | VM on Go |
| Lua | `1,416ms` | `3ms` | Lua (native) |
| [go-lua](https://github.com/Shopify/go-lua) | `4,402ms` | `5ms` | Lua VM on Go |
| [GopherLua](https://github.com/yuin/gopher-lua) | `4,023ms` | `5ms` | Lua VM on Go |
| Python | `2,588ms` | `26ms` | Python (native) |
| [starlark-go](https://github.com/google/starlark-go) | `11,126ms` | `6ms` | Python-like Interpreter on Go |
| [gpython](https://github.com/go-python/gpython) | `15,035ms` | `4ms` | Python Interpreter on Go |
| [goja](https://github.com/dop251/goja) | `5,089ms` | `5ms` | JS VM on Go |
| [otto](https://github.com/robertkrimen/otto) | `68,377ms` | `11ms` | JS Interpreter on Go |
| [Anko](https://github.com/mattn/anko) | `92,579ms` | `18ms` | 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)

View File

@@ -8,9 +8,10 @@ import (
// IdentList represents a list of identifiers. // IdentList represents a list of identifiers.
type IdentList struct { type IdentList struct {
LParen source.Pos LParen source.Pos
List []*Ident VarArgs bool
RParen source.Pos List []*Ident
RParen source.Pos
} }
// Pos returns the position of first character belonging to the node. // Pos returns the position of first character belonging to the node.
@@ -50,8 +51,12 @@ func (n *IdentList) NumFields() int {
func (n *IdentList) String() string { func (n *IdentList) String() string {
var list []string var list []string
for _, e := range n.List { for i, e := range n.List {
list = append(list, e.String()) if n.VarArgs && i == len(n.List)-1 {
list = append(list, "..."+e.String())
} else {
list = append(list, e.String())
}
} }
return "(" + strings.Join(list, ", ") + ")" return "(" + strings.Join(list, ", ") + ")"

View File

@@ -17,32 +17,6 @@ type Bytecode struct {
Constants []objects.Object Constants []objects.Object
} }
// Decode reads Bytecode data from the reader.
func (b *Bytecode) Decode(r io.Reader) error {
dec := gob.NewDecoder(r)
if err := dec.Decode(&b.FileSet); err != nil {
return err
}
// TODO: files in b.FileSet.File does not have their 'set' field properly set to b.FileSet
// as it's private field and not serialized by gob encoder/decoder.
if err := dec.Decode(&b.MainFunction); err != nil {
return err
}
if err := dec.Decode(&b.Constants); err != nil {
return err
}
// replace Bool and Undefined with known value
for i, v := range b.Constants {
b.Constants[i] = cleanupObjects(v)
}
return nil
}
// Encode writes Bytecode data to the writer. // Encode writes Bytecode data to the writer.
func (b *Bytecode) Encode(w io.Writer) error { func (b *Bytecode) Encode(w io.Writer) error {
enc := gob.NewEncoder(w) enc := gob.NewEncoder(w)
@@ -59,6 +33,17 @@ func (b *Bytecode) Encode(w io.Writer) error {
return enc.Encode(b.Constants) return enc.Encode(b.Constants)
} }
// CountObjects returns the number of objects found in Constants.
func (b *Bytecode) CountObjects() int {
n := 0
for _, c := range b.Constants {
n += objects.CountObjects(c)
}
return n
}
// FormatInstructions returns human readable string representations of // FormatInstructions returns human readable string representations of
// compiled instructions. // compiled instructions.
func (b *Bytecode) FormatInstructions() []string { func (b *Bytecode) FormatInstructions() []string {
@@ -83,51 +68,22 @@ func (b *Bytecode) FormatConstants() (output []string) {
return return
} }
func cleanupObjects(o objects.Object) objects.Object {
switch o := o.(type) {
case *objects.Bool:
if o.IsFalsy() {
return objects.FalseValue
}
return objects.TrueValue
case *objects.Undefined:
return objects.UndefinedValue
case *objects.Array:
for i, v := range o.Value {
o.Value[i] = cleanupObjects(v)
}
case *objects.Map:
for k, v := range o.Value {
o.Value[k] = cleanupObjects(v)
}
}
return o
}
func init() { func init() {
gob.Register(&source.FileSet{}) gob.Register(&source.FileSet{})
gob.Register(&source.File{}) gob.Register(&source.File{})
gob.Register(&objects.Array{}) gob.Register(&objects.Array{})
gob.Register(&objects.ArrayIterator{})
gob.Register(&objects.Bool{}) gob.Register(&objects.Bool{})
gob.Register(&objects.Break{})
gob.Register(&objects.BuiltinFunction{})
gob.Register(&objects.Bytes{}) gob.Register(&objects.Bytes{})
gob.Register(&objects.Char{}) gob.Register(&objects.Char{})
gob.Register(&objects.Closure{}) gob.Register(&objects.Closure{})
gob.Register(&objects.CompiledFunction{}) gob.Register(&objects.CompiledFunction{})
gob.Register(&objects.Continue{})
gob.Register(&objects.Error{}) gob.Register(&objects.Error{})
gob.Register(&objects.Float{}) gob.Register(&objects.Float{})
gob.Register(&objects.ImmutableArray{}) gob.Register(&objects.ImmutableArray{})
gob.Register(&objects.ImmutableMap{}) gob.Register(&objects.ImmutableMap{})
gob.Register(&objects.Int{}) gob.Register(&objects.Int{})
gob.Register(&objects.Map{}) gob.Register(&objects.Map{})
gob.Register(&objects.MapIterator{})
gob.Register(&objects.ReturnValue{})
gob.Register(&objects.String{}) gob.Register(&objects.String{})
gob.Register(&objects.StringIterator{})
gob.Register(&objects.Time{}) gob.Register(&objects.Time{})
gob.Register(&objects.Undefined{}) gob.Register(&objects.Undefined{})
gob.Register(&objects.UserFunction{}) gob.Register(&objects.UserFunction{})

97
vendor/github.com/d5/tengo/compiler/bytecode_decode.go generated vendored Normal file
View File

@@ -0,0 +1,97 @@
package compiler
import (
"encoding/gob"
"fmt"
"io"
"github.com/d5/tengo/objects"
)
// Decode reads Bytecode data from the reader.
func (b *Bytecode) Decode(r io.Reader, modules *objects.ModuleMap) error {
if modules == nil {
modules = objects.NewModuleMap()
}
dec := gob.NewDecoder(r)
if err := dec.Decode(&b.FileSet); err != nil {
return err
}
// TODO: files in b.FileSet.File does not have their 'set' field properly set to b.FileSet
// as it's private field and not serialized by gob encoder/decoder.
if err := dec.Decode(&b.MainFunction); err != nil {
return err
}
if err := dec.Decode(&b.Constants); err != nil {
return err
}
for i, v := range b.Constants {
fv, err := fixDecoded(v, modules)
if err != nil {
return err
}
b.Constants[i] = fv
}
return nil
}
func fixDecoded(o objects.Object, modules *objects.ModuleMap) (objects.Object, error) {
switch o := o.(type) {
case *objects.Bool:
if o.IsFalsy() {
return objects.FalseValue, nil
}
return objects.TrueValue, nil
case *objects.Undefined:
return objects.UndefinedValue, nil
case *objects.Array:
for i, v := range o.Value {
fv, err := fixDecoded(v, modules)
if err != nil {
return nil, err
}
o.Value[i] = fv
}
case *objects.ImmutableArray:
for i, v := range o.Value {
fv, err := fixDecoded(v, modules)
if err != nil {
return nil, err
}
o.Value[i] = fv
}
case *objects.Map:
for k, v := range o.Value {
fv, err := fixDecoded(v, modules)
if err != nil {
return nil, err
}
o.Value[k] = fv
}
case *objects.ImmutableMap:
modName := moduleName(o)
if mod := modules.GetBuiltinModule(modName); mod != nil {
return mod.AsImmutableMap(modName), nil
}
for k, v := range o.Value {
// encoding of user function not supported
if _, isUserFunction := v.(*objects.UserFunction); isUserFunction {
return nil, fmt.Errorf("user function not decodable")
}
fv, err := fixDecoded(v, modules)
if err != nil {
return nil, err
}
o.Value[k] = fv
}
}
return o, nil
}

View File

@@ -0,0 +1,129 @@
package compiler
import (
"fmt"
"github.com/d5/tengo/objects"
)
// RemoveDuplicates finds and remove the duplicate values in Constants.
// Note this function mutates Bytecode.
func (b *Bytecode) RemoveDuplicates() {
var deduped []objects.Object
indexMap := make(map[int]int) // mapping from old constant index to new index
ints := make(map[int64]int)
strings := make(map[string]int)
floats := make(map[float64]int)
chars := make(map[rune]int)
immutableMaps := make(map[string]int) // for modules
for curIdx, c := range b.Constants {
switch c := c.(type) {
case *objects.CompiledFunction:
// add to deduped list
indexMap[curIdx] = len(deduped)
deduped = append(deduped, c)
case *objects.ImmutableMap:
modName := moduleName(c)
newIdx, ok := immutableMaps[modName]
if modName != "" && ok {
indexMap[curIdx] = newIdx
} else {
newIdx = len(deduped)
immutableMaps[modName] = newIdx
indexMap[curIdx] = newIdx
deduped = append(deduped, c)
}
case *objects.Int:
if newIdx, ok := ints[c.Value]; ok {
indexMap[curIdx] = newIdx
} else {
newIdx = len(deduped)
ints[c.Value] = newIdx
indexMap[curIdx] = newIdx
deduped = append(deduped, c)
}
case *objects.String:
if newIdx, ok := strings[c.Value]; ok {
indexMap[curIdx] = newIdx
} else {
newIdx = len(deduped)
strings[c.Value] = newIdx
indexMap[curIdx] = newIdx
deduped = append(deduped, c)
}
case *objects.Float:
if newIdx, ok := floats[c.Value]; ok {
indexMap[curIdx] = newIdx
} else {
newIdx = len(deduped)
floats[c.Value] = newIdx
indexMap[curIdx] = newIdx
deduped = append(deduped, c)
}
case *objects.Char:
if newIdx, ok := chars[c.Value]; ok {
indexMap[curIdx] = newIdx
} else {
newIdx = len(deduped)
chars[c.Value] = newIdx
indexMap[curIdx] = newIdx
deduped = append(deduped, c)
}
default:
panic(fmt.Errorf("unsupported top-level constant type: %s", c.TypeName()))
}
}
// replace with de-duplicated constants
b.Constants = deduped
// update CONST instructions with new indexes
// main function
updateConstIndexes(b.MainFunction.Instructions, indexMap)
// other compiled functions in constants
for _, c := range b.Constants {
switch c := c.(type) {
case *objects.CompiledFunction:
updateConstIndexes(c.Instructions, indexMap)
}
}
}
func updateConstIndexes(insts []byte, indexMap map[int]int) {
i := 0
for i < len(insts) {
op := insts[i]
numOperands := OpcodeOperands[op]
_, read := ReadOperands(numOperands, insts[i+1:])
switch op {
case OpConstant:
curIdx := int(insts[i+2]) | int(insts[i+1])<<8
newIdx, ok := indexMap[curIdx]
if !ok {
panic(fmt.Errorf("constant index not found: %d", curIdx))
}
copy(insts[i:], MakeInstruction(op, newIdx))
case OpClosure:
curIdx := int(insts[i+2]) | int(insts[i+1])<<8
numFree := int(insts[i+3])
newIdx, ok := indexMap[curIdx]
if !ok {
panic(fmt.Errorf("constant index not found: %d", curIdx))
}
copy(insts[i:], MakeInstruction(op, newIdx, numFree))
}
i += 1 + read
}
}
func moduleName(mod *objects.ImmutableMap) string {
if modName, ok := mod.Value["__module_name__"].(*objects.String); ok {
return modName.Value
}
return ""
}

View File

@@ -5,8 +5,7 @@ import "github.com/d5/tengo/compiler/source"
// CompilationScope represents a compiled instructions // CompilationScope represents a compiled instructions
// and the last two instructions that were emitted. // and the last two instructions that were emitted.
type CompilationScope struct { type CompilationScope struct {
instructions []byte instructions []byte
lastInstructions [2]EmittedInstruction symbolInit map[string]bool
symbolInit map[string]bool sourceMap map[int]source.Pos
sourceMap map[int]source.Pos
} }

View File

@@ -3,27 +3,30 @@ package compiler
import ( import (
"fmt" "fmt"
"io" "io"
"io/ioutil"
"path/filepath"
"reflect" "reflect"
"strings"
"github.com/d5/tengo"
"github.com/d5/tengo/compiler/ast" "github.com/d5/tengo/compiler/ast"
"github.com/d5/tengo/compiler/source" "github.com/d5/tengo/compiler/source"
"github.com/d5/tengo/compiler/token" "github.com/d5/tengo/compiler/token"
"github.com/d5/tengo/objects" "github.com/d5/tengo/objects"
"github.com/d5/tengo/stdlib"
) )
// Compiler compiles the AST into a bytecode. // Compiler compiles the AST into a bytecode.
type Compiler struct { type Compiler struct {
file *source.File file *source.File
parent *Compiler parent *Compiler
moduleName string modulePath string
constants []objects.Object constants []objects.Object
symbolTable *SymbolTable symbolTable *SymbolTable
scopes []CompilationScope scopes []CompilationScope
scopeIndex int scopeIndex int
moduleLoader ModuleLoader modules *objects.ModuleMap
builtinModules map[string]bool
compiledModules map[string]*objects.CompiledFunction compiledModules map[string]*objects.CompiledFunction
allowFileImport bool
loops []*Loop loops []*Loop
loopIndex int loopIndex int
trace io.Writer trace io.Writer
@@ -31,12 +34,7 @@ type Compiler struct {
} }
// NewCompiler creates a Compiler. // NewCompiler creates a Compiler.
// User can optionally provide the symbol table if one wants to add or remove func NewCompiler(file *source.File, symbolTable *SymbolTable, constants []objects.Object, modules *objects.ModuleMap, trace io.Writer) *Compiler {
// some global- or builtin- scope symbols. If not (nil), Compile will create
// a new symbol table and use the default builtin functions. Likewise, standard
// modules can be explicitly provided if user wants to add or remove some modules.
// By default, Compile will use all the standard modules otherwise.
func NewCompiler(file *source.File, symbolTable *SymbolTable, constants []objects.Object, builtinModules map[string]bool, trace io.Writer) *Compiler {
mainScope := CompilationScope{ mainScope := CompilationScope{
symbolInit: make(map[string]bool), symbolInit: make(map[string]bool),
sourceMap: make(map[int]source.Pos), sourceMap: make(map[int]source.Pos),
@@ -45,18 +43,16 @@ func NewCompiler(file *source.File, symbolTable *SymbolTable, constants []object
// symbol table // symbol table
if symbolTable == nil { if symbolTable == nil {
symbolTable = NewSymbolTable() symbolTable = NewSymbolTable()
}
for idx, fn := range objects.Builtins { // add builtin functions to the symbol table
symbolTable.DefineBuiltin(idx, fn.Name) for idx, fn := range objects.Builtins {
} symbolTable.DefineBuiltin(idx, fn.Name)
} }
// builtin modules // builtin modules
if builtinModules == nil { if modules == nil {
builtinModules = make(map[string]bool) modules = objects.NewModuleMap()
for name := range stdlib.Modules {
builtinModules[name] = true
}
} }
return &Compiler{ return &Compiler{
@@ -67,7 +63,7 @@ func NewCompiler(file *source.File, symbolTable *SymbolTable, constants []object
scopeIndex: 0, scopeIndex: 0,
loopIndex: -1, loopIndex: -1,
trace: trace, trace: trace,
builtinModules: builtinModules, modules: modules,
compiledModules: make(map[string]*objects.CompiledFunction), compiledModules: make(map[string]*objects.CompiledFunction),
} }
} }
@@ -123,7 +119,7 @@ func (c *Compiler) Compile(node ast.Node) error {
return err return err
} }
c.emit(node, OpGreaterThan) c.emit(node, OpBinaryOp, int(token.Greater))
return nil return nil
} else if node.Token == token.LessEq { } else if node.Token == token.LessEq {
@@ -134,7 +130,7 @@ func (c *Compiler) Compile(node ast.Node) error {
return err return err
} }
c.emit(node, OpGreaterThanEqual) c.emit(node, OpBinaryOp, int(token.GreaterEq))
return nil return nil
} }
@@ -148,35 +144,35 @@ func (c *Compiler) Compile(node ast.Node) error {
switch node.Token { switch node.Token {
case token.Add: case token.Add:
c.emit(node, OpAdd) c.emit(node, OpBinaryOp, int(token.Add))
case token.Sub: case token.Sub:
c.emit(node, OpSub) c.emit(node, OpBinaryOp, int(token.Sub))
case token.Mul: case token.Mul:
c.emit(node, OpMul) c.emit(node, OpBinaryOp, int(token.Mul))
case token.Quo: case token.Quo:
c.emit(node, OpDiv) c.emit(node, OpBinaryOp, int(token.Quo))
case token.Rem: case token.Rem:
c.emit(node, OpRem) c.emit(node, OpBinaryOp, int(token.Rem))
case token.Greater: case token.Greater:
c.emit(node, OpGreaterThan) c.emit(node, OpBinaryOp, int(token.Greater))
case token.GreaterEq: case token.GreaterEq:
c.emit(node, OpGreaterThanEqual) c.emit(node, OpBinaryOp, int(token.GreaterEq))
case token.Equal: case token.Equal:
c.emit(node, OpEqual) c.emit(node, OpEqual)
case token.NotEqual: case token.NotEqual:
c.emit(node, OpNotEqual) c.emit(node, OpNotEqual)
case token.And: case token.And:
c.emit(node, OpBAnd) c.emit(node, OpBinaryOp, int(token.And))
case token.Or: case token.Or:
c.emit(node, OpBOr) c.emit(node, OpBinaryOp, int(token.Or))
case token.Xor: case token.Xor:
c.emit(node, OpBXor) c.emit(node, OpBinaryOp, int(token.Xor))
case token.AndNot: case token.AndNot:
c.emit(node, OpBAndNot) c.emit(node, OpBinaryOp, int(token.AndNot))
case token.Shl: case token.Shl:
c.emit(node, OpBShiftLeft) c.emit(node, OpBinaryOp, int(token.Shl))
case token.Shr: case token.Shr:
c.emit(node, OpBShiftRight) c.emit(node, OpBinaryOp, int(token.Shr))
default: default:
return c.errorf(node, "invalid binary operator: %s", node.Token.String()) return c.errorf(node, "invalid binary operator: %s", node.Token.String())
} }
@@ -195,6 +191,10 @@ func (c *Compiler) Compile(node ast.Node) error {
} }
case *ast.StringLit: case *ast.StringLit:
if len(node.Value) > tengo.MaxStringLen {
return c.error(node, objects.ErrStringLimit)
}
c.emit(node, OpConstant, c.addConstant(&objects.String{Value: node.Value})) c.emit(node, OpConstant, c.addConstant(&objects.String{Value: node.Value}))
case *ast.CharLit: case *ast.CharLit:
@@ -292,6 +292,15 @@ func (c *Compiler) Compile(node ast.Node) error {
} }
case *ast.BlockStmt: case *ast.BlockStmt:
if len(node.Stmts) == 0 {
return nil
}
c.symbolTable = c.symbolTable.Fork(true)
defer func() {
c.symbolTable = c.symbolTable.Parent(false)
}()
for _, stmt := range node.Stmts { for _, stmt := range node.Stmts {
if err := c.Compile(stmt); err != nil { if err := c.Compile(stmt); err != nil {
return err return err
@@ -332,6 +341,9 @@ func (c *Compiler) Compile(node ast.Node) error {
case *ast.MapLit: case *ast.MapLit:
for _, elt := range node.Elements { for _, elt := range node.Elements {
// key // key
if len(elt.Key) > tengo.MaxStringLen {
return c.error(node, objects.ErrStringLimit)
}
c.emit(node, OpConstant, c.addConstant(&objects.String{Value: elt.Key})) c.emit(node, OpConstant, c.addConstant(&objects.String{Value: elt.Key}))
// value // value
@@ -401,10 +413,8 @@ func (c *Compiler) Compile(node ast.Node) error {
return err return err
} }
// add OpReturn if function returns nothing // code optimization
if !c.lastInstructionIs(OpReturnValue) && !c.lastInstructionIs(OpReturn) { c.optimizeFunc(node)
c.emit(node, OpReturn)
}
freeSymbols := c.symbolTable.FreeSymbols() freeSymbols := c.symbolTable.FreeSymbols()
numLocals := c.symbolTable.MaxSymbols() numLocals := c.symbolTable.MaxSymbols()
@@ -457,9 +467,9 @@ func (c *Compiler) Compile(node ast.Node) error {
s.LocalAssigned = true s.LocalAssigned = true
} }
c.emit(node, OpGetLocal, s.Index) c.emit(node, OpGetLocalPtr, s.Index)
case ScopeFree: case ScopeFree:
c.emit(node, OpGetFree, s.Index) c.emit(node, OpGetFreePtr, s.Index)
} }
} }
@@ -467,6 +477,7 @@ func (c *Compiler) Compile(node ast.Node) error {
Instructions: instructions, Instructions: instructions,
NumLocals: numLocals, NumLocals: numLocals,
NumParameters: len(node.Type.Params.List), NumParameters: len(node.Type.Params.List),
VarArgs: node.Type.Params.VarArgs,
SourceMap: sourceMap, SourceMap: sourceMap,
} }
@@ -483,13 +494,13 @@ func (c *Compiler) Compile(node ast.Node) error {
} }
if node.Result == nil { if node.Result == nil {
c.emit(node, OpReturn) c.emit(node, OpReturn, 0)
} else { } else {
if err := c.Compile(node.Result); err != nil { if err := c.Compile(node.Result); err != nil {
return err return err
} }
c.emit(node, OpReturnValue) c.emit(node, OpReturn, 1)
} }
case *ast.CallExpr: case *ast.CallExpr:
@@ -506,17 +517,57 @@ func (c *Compiler) Compile(node ast.Node) error {
c.emit(node, OpCall, len(node.Args)) c.emit(node, OpCall, len(node.Args))
case *ast.ImportExpr: case *ast.ImportExpr:
if c.builtinModules[node.ModuleName] { if node.ModuleName == "" {
c.emit(node, OpConstant, c.addConstant(&objects.String{Value: node.ModuleName})) return c.errorf(node, "empty module name")
c.emit(node, OpGetBuiltinModule) }
} else {
userMod, err := c.compileModule(node) if mod := c.modules.Get(node.ModuleName); mod != nil {
v, err := mod.Import(node.ModuleName)
if err != nil { if err != nil {
return err return err
} }
c.emit(node, OpConstant, c.addConstant(userMod)) switch v := v.(type) {
case []byte: // module written in Tengo
compiled, err := c.compileModule(node, node.ModuleName, node.ModuleName, v)
if err != nil {
return err
}
c.emit(node, OpConstant, c.addConstant(compiled))
c.emit(node, OpCall, 0)
case objects.Object: // builtin module
c.emit(node, OpConstant, c.addConstant(v))
default:
panic(fmt.Errorf("invalid import value type: %T", v))
}
} else if c.allowFileImport {
moduleName := node.ModuleName
if !strings.HasSuffix(moduleName, ".tengo") {
moduleName += ".tengo"
}
modulePath, err := filepath.Abs(moduleName)
if err != nil {
return c.errorf(node, "module file path error: %s", err.Error())
}
if err := c.checkCyclicImports(node, modulePath); err != nil {
return err
}
moduleSrc, err := ioutil.ReadFile(moduleName)
if err != nil {
return c.errorf(node, "module file read error: %s", err.Error())
}
compiled, err := c.compileModule(node, moduleName, modulePath, moduleSrc)
if err != nil {
return err
}
c.emit(node, OpConstant, c.addConstant(compiled))
c.emit(node, OpCall, 0) c.emit(node, OpCall, 0)
} else {
return c.errorf(node, "module '%s' not found", node.ModuleName)
} }
case *ast.ExportStmt: case *ast.ExportStmt:
@@ -535,7 +586,7 @@ func (c *Compiler) Compile(node ast.Node) error {
} }
c.emit(node, OpImmutable) c.emit(node, OpImmutable)
c.emit(node, OpReturnValue) c.emit(node, OpReturn, 1)
case *ast.ErrorExpr: case *ast.ErrorExpr:
if err := c.Compile(node.Expr); err != nil { if err := c.Compile(node.Expr); err != nil {
@@ -594,22 +645,28 @@ func (c *Compiler) Bytecode() *Bytecode {
} }
} }
// SetModuleLoader sets or replaces the current module loader. // EnableFileImport enables or disables module loading from local files.
// Note that the module loader is used for user modules, // Local file modules are disabled by default.
// not for the standard modules. func (c *Compiler) EnableFileImport(enable bool) {
func (c *Compiler) SetModuleLoader(moduleLoader ModuleLoader) { c.allowFileImport = enable
c.moduleLoader = moduleLoader
} }
func (c *Compiler) fork(file *source.File, moduleName string, symbolTable *SymbolTable) *Compiler { func (c *Compiler) fork(file *source.File, modulePath string, symbolTable *SymbolTable) *Compiler {
child := NewCompiler(file, symbolTable, nil, c.builtinModules, c.trace) child := NewCompiler(file, symbolTable, nil, c.modules, c.trace)
child.moduleName = moduleName // name of the module to compile child.modulePath = modulePath // module file path
child.parent = c // parent to set to current compiler child.parent = c // parent to set to current compiler
child.moduleLoader = c.moduleLoader // share module loader
return child return child
} }
func (c *Compiler) error(node ast.Node, err error) error {
return &Error{
fileSet: c.file.Set(),
node: node,
error: err,
}
}
func (c *Compiler) errorf(node ast.Node, format string, args ...interface{}) error { func (c *Compiler) errorf(node ast.Node, format string, args ...interface{}) error {
return &Error{ return &Error{
fileSet: c.file.Set(), fileSet: c.file.Set(),
@@ -641,33 +698,6 @@ func (c *Compiler) addInstruction(b []byte) int {
return posNewIns return posNewIns
} }
func (c *Compiler) setLastInstruction(op Opcode, pos int) {
c.scopes[c.scopeIndex].lastInstructions[1] = c.scopes[c.scopeIndex].lastInstructions[0]
c.scopes[c.scopeIndex].lastInstructions[0].Opcode = op
c.scopes[c.scopeIndex].lastInstructions[0].Position = pos
}
func (c *Compiler) lastInstructionIs(op Opcode) bool {
if len(c.currentInstructions()) == 0 {
return false
}
return c.scopes[c.scopeIndex].lastInstructions[0].Opcode == op
}
func (c *Compiler) removeLastInstruction() {
lastPos := c.scopes[c.scopeIndex].lastInstructions[0].Position
if c.trace != nil {
c.printTrace(fmt.Sprintf("DELET %s",
FormatInstructions(c.scopes[c.scopeIndex].instructions[lastPos:], lastPos)[0]))
}
c.scopes[c.scopeIndex].instructions = c.currentInstructions()[:lastPos]
c.scopes[c.scopeIndex].lastInstructions[0] = c.scopes[c.scopeIndex].lastInstructions[1]
}
func (c *Compiler) replaceInstruction(pos int, inst []byte) { func (c *Compiler) replaceInstruction(pos int, inst []byte) {
copy(c.currentInstructions()[pos:], inst) copy(c.currentInstructions()[pos:], inst)
@@ -684,6 +714,92 @@ func (c *Compiler) changeOperand(opPos int, operand ...int) {
c.replaceInstruction(opPos, inst) c.replaceInstruction(opPos, inst)
} }
// optimizeFunc performs some code-level optimization for the current function instructions
// it removes unreachable (dead code) instructions and adds "returns" instruction if needed.
func (c *Compiler) optimizeFunc(node ast.Node) {
// any instructions between RETURN and the function end
// or instructions between RETURN and jump target position
// are considered as unreachable.
// pass 1. identify all jump destinations
dsts := make(map[int]bool)
iterateInstructions(c.scopes[c.scopeIndex].instructions, func(pos int, opcode Opcode, operands []int) bool {
switch opcode {
case OpJump, OpJumpFalsy, OpAndJump, OpOrJump:
dsts[operands[0]] = true
}
return true
})
var newInsts []byte
// pass 2. eliminate dead code
posMap := make(map[int]int) // old position to new position
var dstIdx int
var deadCode bool
iterateInstructions(c.scopes[c.scopeIndex].instructions, func(pos int, opcode Opcode, operands []int) bool {
switch {
case opcode == OpReturn:
if deadCode {
return true
}
deadCode = true
case dsts[pos]:
dstIdx++
deadCode = false
case deadCode:
return true
}
posMap[pos] = len(newInsts)
newInsts = append(newInsts, MakeInstruction(opcode, operands...)...)
return true
})
// pass 3. update jump positions
var lastOp Opcode
var appendReturn bool
endPos := len(c.scopes[c.scopeIndex].instructions)
iterateInstructions(newInsts, func(pos int, opcode Opcode, operands []int) bool {
switch opcode {
case OpJump, OpJumpFalsy, OpAndJump, OpOrJump:
newDst, ok := posMap[operands[0]]
if ok {
copy(newInsts[pos:], MakeInstruction(opcode, newDst))
} else if endPos == operands[0] {
// there's a jump instruction that jumps to the end of function
// compiler should append "return".
appendReturn = true
} else {
panic(fmt.Errorf("invalid jump position: %d", newDst))
}
}
lastOp = opcode
return true
})
if lastOp != OpReturn {
appendReturn = true
}
// pass 4. update source map
newSourceMap := make(map[int]source.Pos)
for pos, srcPos := range c.scopes[c.scopeIndex].sourceMap {
newPos, ok := posMap[pos]
if ok {
newSourceMap[newPos] = srcPos
}
}
c.scopes[c.scopeIndex].instructions = newInsts
c.scopes[c.scopeIndex].sourceMap = newSourceMap
// append "return"
if appendReturn {
c.emit(node, OpReturn, 0)
}
}
func (c *Compiler) emit(node ast.Node, opcode Opcode, operands ...int) int { func (c *Compiler) emit(node ast.Node, opcode Opcode, operands ...int) int {
filePos := source.NoPos filePos := source.NoPos
if node != nil { if node != nil {
@@ -693,7 +809,6 @@ func (c *Compiler) emit(node ast.Node, opcode Opcode, operands ...int) int {
inst := MakeInstruction(opcode, operands...) inst := MakeInstruction(opcode, operands...)
pos := c.addInstruction(inst) pos := c.addInstruction(inst)
c.scopes[c.scopeIndex].sourceMap[pos] = filePos c.scopes[c.scopeIndex].sourceMap[pos] = filePos
c.setLastInstruction(opcode, pos)
if c.trace != nil { if c.trace != nil {
c.printTrace(fmt.Sprintf("EMIT %s", c.printTrace(fmt.Sprintf("EMIT %s",

View File

@@ -51,27 +51,27 @@ func (c *Compiler) compileAssign(node ast.Node, lhs, rhs []ast.Expr, op token.To
switch op { switch op {
case token.AddAssign: case token.AddAssign:
c.emit(node, OpAdd) c.emit(node, OpBinaryOp, int(token.Add))
case token.SubAssign: case token.SubAssign:
c.emit(node, OpSub) c.emit(node, OpBinaryOp, int(token.Sub))
case token.MulAssign: case token.MulAssign:
c.emit(node, OpMul) c.emit(node, OpBinaryOp, int(token.Mul))
case token.QuoAssign: case token.QuoAssign:
c.emit(node, OpDiv) c.emit(node, OpBinaryOp, int(token.Quo))
case token.RemAssign: case token.RemAssign:
c.emit(node, OpRem) c.emit(node, OpBinaryOp, int(token.Rem))
case token.AndAssign: case token.AndAssign:
c.emit(node, OpBAnd) c.emit(node, OpBinaryOp, int(token.And))
case token.OrAssign: case token.OrAssign:
c.emit(node, OpBOr) c.emit(node, OpBinaryOp, int(token.Or))
case token.AndNotAssign: case token.AndNotAssign:
c.emit(node, OpBAndNot) c.emit(node, OpBinaryOp, int(token.AndNot))
case token.XorAssign: case token.XorAssign:
c.emit(node, OpBXor) c.emit(node, OpBinaryOp, int(token.Xor))
case token.ShlAssign: case token.ShlAssign:
c.emit(node, OpBShiftLeft) c.emit(node, OpBinaryOp, int(token.Shl))
case token.ShrAssign: case token.ShrAssign:
c.emit(node, OpBShiftRight) c.emit(node, OpBinaryOp, int(token.Shr))
} }
// compile selector expressions (right to left) // compile selector expressions (right to left)

View File

@@ -1,72 +1,31 @@
package compiler package compiler
import ( import (
"io/ioutil"
"strings"
"github.com/d5/tengo/compiler/ast" "github.com/d5/tengo/compiler/ast"
"github.com/d5/tengo/compiler/parser" "github.com/d5/tengo/compiler/parser"
"github.com/d5/tengo/objects" "github.com/d5/tengo/objects"
) )
func (c *Compiler) compileModule(expr *ast.ImportExpr) (*objects.CompiledFunction, error) { func (c *Compiler) checkCyclicImports(node ast.Node, modulePath string) error {
compiledModule, exists := c.loadCompiledModule(expr.ModuleName) if c.modulePath == modulePath {
if exists { return c.errorf(node, "cyclic module import: %s", modulePath)
return compiledModule, nil
}
moduleName := expr.ModuleName
// read module source from loader
var moduleSrc []byte
if c.moduleLoader == nil {
// default loader: read from local file
if !strings.HasSuffix(moduleName, ".tengo") {
moduleName += ".tengo"
}
if err := c.checkCyclicImports(expr, moduleName); err != nil {
return nil, err
}
var err error
moduleSrc, err = ioutil.ReadFile(moduleName)
if err != nil {
return nil, c.errorf(expr, "module file read error: %s", err.Error())
}
} else {
if err := c.checkCyclicImports(expr, moduleName); err != nil {
return nil, err
}
var err error
moduleSrc, err = c.moduleLoader(moduleName)
if err != nil {
return nil, err
}
}
compiledModule, err := c.doCompileModule(moduleName, moduleSrc)
if err != nil {
return nil, err
}
c.storeCompiledModule(moduleName, compiledModule)
return compiledModule, nil
}
func (c *Compiler) checkCyclicImports(node ast.Node, moduleName string) error {
if c.moduleName == moduleName {
return c.errorf(node, "cyclic module import: %s", moduleName)
} else if c.parent != nil { } else if c.parent != nil {
return c.parent.checkCyclicImports(node, moduleName) return c.parent.checkCyclicImports(node, modulePath)
} }
return nil return nil
} }
func (c *Compiler) doCompileModule(moduleName string, src []byte) (*objects.CompiledFunction, error) { func (c *Compiler) compileModule(node ast.Node, moduleName, modulePath string, src []byte) (*objects.CompiledFunction, error) {
if err := c.checkCyclicImports(node, modulePath); err != nil {
return nil, err
}
compiledModule, exists := c.loadCompiledModule(modulePath)
if exists {
return compiledModule, nil
}
modFile := c.file.Set().AddFile(moduleName, -1, len(src)) modFile := c.file.Set().AddFile(moduleName, -1, len(src))
p := parser.NewParser(modFile, src, nil) p := parser.NewParser(modFile, src, nil)
file, err := p.ParseFile() file, err := p.ParseFile()
@@ -77,47 +36,44 @@ func (c *Compiler) doCompileModule(moduleName string, src []byte) (*objects.Comp
symbolTable := NewSymbolTable() symbolTable := NewSymbolTable()
// inherit builtin functions // inherit builtin functions
for idx, fn := range objects.Builtins { for _, sym := range c.symbolTable.BuiltinSymbols() {
s, _, ok := c.symbolTable.Resolve(fn.Name) symbolTable.DefineBuiltin(sym.Index, sym.Name)
if ok && s.Scope == ScopeBuiltin {
symbolTable.DefineBuiltin(idx, fn.Name)
}
} }
// no global scope for the module // no global scope for the module
symbolTable = symbolTable.Fork(false) symbolTable = symbolTable.Fork(false)
// compile module // compile module
moduleCompiler := c.fork(modFile, moduleName, symbolTable) moduleCompiler := c.fork(modFile, modulePath, symbolTable)
if err := moduleCompiler.Compile(file); err != nil { if err := moduleCompiler.Compile(file); err != nil {
return nil, err return nil, err
} }
// add OpReturn (== export undefined) if export is missing // code optimization
if !moduleCompiler.lastInstructionIs(OpReturnValue) { moduleCompiler.optimizeFunc(node)
moduleCompiler.emit(nil, OpReturn)
}
compiledFunc := moduleCompiler.Bytecode().MainFunction compiledFunc := moduleCompiler.Bytecode().MainFunction
compiledFunc.NumLocals = symbolTable.MaxSymbols() compiledFunc.NumLocals = symbolTable.MaxSymbols()
c.storeCompiledModule(modulePath, compiledFunc)
return compiledFunc, nil return compiledFunc, nil
} }
func (c *Compiler) loadCompiledModule(moduleName string) (mod *objects.CompiledFunction, ok bool) { func (c *Compiler) loadCompiledModule(modulePath string) (mod *objects.CompiledFunction, ok bool) {
if c.parent != nil { if c.parent != nil {
return c.parent.loadCompiledModule(moduleName) return c.parent.loadCompiledModule(modulePath)
} }
mod, ok = c.compiledModules[moduleName] mod, ok = c.compiledModules[modulePath]
return return
} }
func (c *Compiler) storeCompiledModule(moduleName string, module *objects.CompiledFunction) { func (c *Compiler) storeCompiledModule(modulePath string, module *objects.CompiledFunction) {
if c.parent != nil { if c.parent != nil {
c.parent.storeCompiledModule(moduleName, module) c.parent.storeCompiledModule(modulePath, module)
} }
c.compiledModules[moduleName] = module c.compiledModules[modulePath] = module
} }

View File

@@ -13,7 +13,7 @@ func MakeInstruction(opcode Opcode, operands ...int) []byte {
totalLen += w totalLen += w
} }
instruction := make([]byte, totalLen, totalLen) instruction := make([]byte, totalLen)
instruction[0] = byte(opcode) instruction[0] = byte(opcode)
offset := 1 offset := 1
@@ -57,3 +57,16 @@ func FormatInstructions(b []byte, posOffset int) []string {
return out return out
} }
func iterateInstructions(b []byte, fn func(pos int, opcode Opcode, operands []int) bool) {
for i := 0; i < len(b); i++ {
numOperands := OpcodeOperands[Opcode(b[i])]
operands, read := ReadOperands(numOperands, b[i+1:])
if !fn(i, b[i], operands) {
break
}
i += read
}
}

View File

@@ -5,173 +5,137 @@ type Opcode = byte
// List of opcodes // List of opcodes
const ( const (
OpConstant Opcode = iota // Load constant OpConstant Opcode = iota // Load constant
OpAdd // Add OpBComplement // bitwise complement
OpSub // Sub OpPop // Pop
OpMul // Multiply OpTrue // Push true
OpDiv // Divide OpFalse // Push false
OpRem // Remainder OpEqual // Equal ==
OpBAnd // bitwise AND OpNotEqual // Not equal !=
OpBOr // bitwise OR OpMinus // Minus -
OpBXor // bitwise XOR OpLNot // Logical not !
OpBShiftLeft // bitwise shift left OpJumpFalsy // Jump if falsy
OpBShiftRight // bitwise shift right OpAndJump // Logical AND jump
OpBAndNot // bitwise AND NOT OpOrJump // Logical OR jump
OpBComplement // bitwise complement OpJump // Jump
OpPop // Pop OpNull // Push null
OpTrue // Push true OpArray // Array object
OpFalse // Push false OpMap // Map object
OpEqual // Equal == OpError // Error object
OpNotEqual // Not equal != OpImmutable // Immutable object
OpGreaterThan // Greater than >= OpIndex // Index operation
OpGreaterThanEqual // Greater than or equal to >= OpSliceIndex // Slice operation
OpMinus // Minus - OpCall // Call function
OpLNot // Logical not ! OpReturn // Return
OpJumpFalsy // Jump if falsy OpGetGlobal // Get global variable
OpAndJump // Logical AND jump OpSetGlobal // Set global variable
OpOrJump // Logical OR jump OpSetSelGlobal // Set global variable using selectors
OpJump // Jump OpGetLocal // Get local variable
OpNull // Push null OpSetLocal // Set local variable
OpArray // Array object OpDefineLocal // Define local variable
OpMap // Map object OpSetSelLocal // Set local variable using selectors
OpError // Error object OpGetFreePtr // Get free variable pointer object
OpImmutable // Immutable object OpGetFree // Get free variables
OpIndex // Index operation OpSetFree // Set free variables
OpSliceIndex // Slice operation OpGetLocalPtr // Get local variable as a pointer
OpCall // Call function OpSetSelFree // Set free variables using selectors
OpReturn // Return OpGetBuiltin // Get builtin function
OpReturnValue // Return value OpClosure // Push closure
OpGetGlobal // Get global variable OpIteratorInit // Iterator init
OpSetGlobal // Set global variable OpIteratorNext // Iterator next
OpSetSelGlobal // Set global variable using selectors OpIteratorKey // Iterator key
OpGetLocal // Get local variable OpIteratorValue // Iterator value
OpSetLocal // Set local variable OpBinaryOp // Binary Operation
OpDefineLocal // Define local variable
OpSetSelLocal // Set local variable using selectors
OpGetFree // Get free variables
OpSetFree // Set free variables
OpSetSelFree // Set free variables using selectors
OpGetBuiltin // Get builtin function
OpGetBuiltinModule // Get builtin module
OpClosure // Push closure
OpIteratorInit // Iterator init
OpIteratorNext // Iterator next
OpIteratorKey // Iterator key
OpIteratorValue // Iterator value
) )
// OpcodeNames is opcode names. // OpcodeNames is opcode names.
var OpcodeNames = [...]string{ var OpcodeNames = [...]string{
OpConstant: "CONST", OpConstant: "CONST",
OpPop: "POP", OpPop: "POP",
OpTrue: "TRUE", OpTrue: "TRUE",
OpFalse: "FALSE", OpFalse: "FALSE",
OpAdd: "ADD", OpBComplement: "NEG",
OpSub: "SUB", OpEqual: "EQL",
OpMul: "MUL", OpNotEqual: "NEQ",
OpDiv: "DIV", OpMinus: "NEG",
OpRem: "REM", OpLNot: "NOT",
OpBAnd: "AND", OpJumpFalsy: "JMPF",
OpBOr: "OR", OpAndJump: "ANDJMP",
OpBXor: "XOR", OpOrJump: "ORJMP",
OpBAndNot: "ANDN", OpJump: "JMP",
OpBShiftLeft: "SHL", OpNull: "NULL",
OpBShiftRight: "SHR", OpGetGlobal: "GETG",
OpBComplement: "NEG", OpSetGlobal: "SETG",
OpEqual: "EQL", OpSetSelGlobal: "SETSG",
OpNotEqual: "NEQ", OpArray: "ARR",
OpGreaterThan: "GTR", OpMap: "MAP",
OpGreaterThanEqual: "GEQ", OpError: "ERROR",
OpMinus: "NEG", OpImmutable: "IMMUT",
OpLNot: "NOT", OpIndex: "INDEX",
OpJumpFalsy: "JMPF", OpSliceIndex: "SLICE",
OpAndJump: "ANDJMP", OpCall: "CALL",
OpOrJump: "ORJMP", OpReturn: "RET",
OpJump: "JMP", OpGetLocal: "GETL",
OpNull: "NULL", OpSetLocal: "SETL",
OpGetGlobal: "GETG", OpDefineLocal: "DEFL",
OpSetGlobal: "SETG", OpSetSelLocal: "SETSL",
OpSetSelGlobal: "SETSG", OpGetBuiltin: "BUILTIN",
OpArray: "ARR", OpClosure: "CLOSURE",
OpMap: "MAP", OpGetFreePtr: "GETFP",
OpError: "ERROR", OpGetFree: "GETF",
OpImmutable: "IMMUT", OpSetFree: "SETF",
OpIndex: "INDEX", OpGetLocalPtr: "GETLP",
OpSliceIndex: "SLICE", OpSetSelFree: "SETSF",
OpCall: "CALL", OpIteratorInit: "ITER",
OpReturn: "RET", OpIteratorNext: "ITNXT",
OpReturnValue: "RETVAL", OpIteratorKey: "ITKEY",
OpGetLocal: "GETL", OpIteratorValue: "ITVAL",
OpSetLocal: "SETL", OpBinaryOp: "BINARYOP",
OpDefineLocal: "DEFL",
OpSetSelLocal: "SETSL",
OpGetBuiltin: "BUILTIN",
OpGetBuiltinModule: "BLTMOD",
OpClosure: "CLOSURE",
OpGetFree: "GETF",
OpSetFree: "SETF",
OpSetSelFree: "SETSF",
OpIteratorInit: "ITER",
OpIteratorNext: "ITNXT",
OpIteratorKey: "ITKEY",
OpIteratorValue: "ITVAL",
} }
// OpcodeOperands is the number of operands. // OpcodeOperands is the number of operands.
var OpcodeOperands = [...][]int{ var OpcodeOperands = [...][]int{
OpConstant: {2}, OpConstant: {2},
OpPop: {}, OpPop: {},
OpTrue: {}, OpTrue: {},
OpFalse: {}, OpFalse: {},
OpAdd: {}, OpBComplement: {},
OpSub: {}, OpEqual: {},
OpMul: {}, OpNotEqual: {},
OpDiv: {}, OpMinus: {},
OpRem: {}, OpLNot: {},
OpBAnd: {}, OpJumpFalsy: {2},
OpBOr: {}, OpAndJump: {2},
OpBXor: {}, OpOrJump: {2},
OpBAndNot: {}, OpJump: {2},
OpBShiftLeft: {}, OpNull: {},
OpBShiftRight: {}, OpGetGlobal: {2},
OpBComplement: {}, OpSetGlobal: {2},
OpEqual: {}, OpSetSelGlobal: {2, 1},
OpNotEqual: {}, OpArray: {2},
OpGreaterThan: {}, OpMap: {2},
OpGreaterThanEqual: {}, OpError: {},
OpMinus: {}, OpImmutable: {},
OpLNot: {}, OpIndex: {},
OpJumpFalsy: {2}, OpSliceIndex: {},
OpAndJump: {2}, OpCall: {1},
OpOrJump: {2}, OpReturn: {1},
OpJump: {2}, OpGetLocal: {1},
OpNull: {}, OpSetLocal: {1},
OpGetGlobal: {2}, OpDefineLocal: {1},
OpSetGlobal: {2}, OpSetSelLocal: {1, 1},
OpSetSelGlobal: {2, 1}, OpGetBuiltin: {1},
OpArray: {2}, OpClosure: {2, 1},
OpMap: {2}, OpGetFreePtr: {1},
OpError: {}, OpGetFree: {1},
OpImmutable: {}, OpSetFree: {1},
OpIndex: {}, OpGetLocalPtr: {1},
OpSliceIndex: {}, OpSetSelFree: {1, 1},
OpCall: {1}, OpIteratorInit: {},
OpReturn: {}, OpIteratorNext: {},
OpReturnValue: {}, OpIteratorKey: {},
OpGetLocal: {1}, OpIteratorValue: {},
OpSetLocal: {1}, OpBinaryOp: {1},
OpDefineLocal: {1},
OpSetSelLocal: {1, 1},
OpGetBuiltin: {1},
OpGetBuiltinModule: {},
OpClosure: {2, 1},
OpGetFree: {1},
OpSetFree: {1},
OpSetSelFree: {1, 1},
OpIteratorInit: {},
OpIteratorNext: {},
OpIteratorKey: {},
OpIteratorValue: {},
} }
// ReadOperands reads operands from the bytecode. // ReadOperands reads operands from the bytecode.

View File

@@ -1,28 +0,0 @@
package parser
import (
"io"
"github.com/d5/tengo/compiler/ast"
"github.com/d5/tengo/compiler/source"
)
// ParseFile parses a file with a given src.
func ParseFile(file *source.File, src []byte, trace io.Writer) (res *ast.File, err error) {
p := NewParser(file, src, trace)
defer func() {
if e := recover(); e != nil {
if _, ok := e.(bailout); !ok {
panic(e)
}
}
p.errors.Sort()
err = p.errors.Err()
}()
res, err = p.ParseFile()
return
}

View File

@@ -12,5 +12,6 @@ func ParseSource(filename string, src []byte, trace io.Writer) (res *ast.File, e
fileSet := source.NewFileSet() fileSet := source.NewFileSet()
file := fileSet.AddFile(filename, -1, len(src)) file := fileSet.AddFile(filename, -1, len(src))
return ParseFile(file, src, trace) p := NewParser(file, src, trace)
return p.ParseFile()
} }

View File

@@ -57,7 +57,18 @@ func NewParser(file *source.File, src []byte, trace io.Writer) *Parser {
} }
// ParseFile parses the source and returns an AST file unit. // ParseFile parses the source and returns an AST file unit.
func (p *Parser) ParseFile() (*ast.File, error) { func (p *Parser) ParseFile() (file *ast.File, err error) {
defer func() {
if e := recover(); e != nil {
if _, ok := e.(bailout); !ok {
panic(e)
}
}
p.errors.Sort()
err = p.errors.Err()
}()
if p.trace { if p.trace {
defer un(trace(p, "File")) defer un(trace(p, "File"))
} }
@@ -71,10 +82,12 @@ func (p *Parser) ParseFile() (*ast.File, error) {
return nil, p.errors.Err() return nil, p.errors.Err()
} }
return &ast.File{ file = &ast.File{
InputFile: p.file, InputFile: p.file,
Stmts: stmts, Stmts: stmts,
}, nil }
return
} }
func (p *Parser) parseExpr() ast.Expr { func (p *Parser) parseExpr() ast.Expr {
@@ -597,19 +610,31 @@ func (p *Parser) parseIdentList() *ast.IdentList {
var params []*ast.Ident var params []*ast.Ident
lparen := p.expect(token.LParen) lparen := p.expect(token.LParen)
isVarArgs := false
if p.token != token.RParen { if p.token != token.RParen {
params = append(params, p.parseIdent()) if p.token == token.Ellipsis {
for p.token == token.Comma { isVarArgs = true
p.next() p.next()
}
params = append(params, p.parseIdent())
for !isVarArgs && p.token == token.Comma {
p.next()
if p.token == token.Ellipsis {
isVarArgs = true
p.next()
}
params = append(params, p.parseIdent()) params = append(params, p.parseIdent())
} }
} }
rparen := p.expect(token.RParen) rparen := p.expect(token.RParen)
return &ast.IdentList{ return &ast.IdentList{
LParen: lparen, LParen: lparen,
RParen: rparen, RParen: rparen,
List: params, VarArgs: isVarArgs,
List: params,
} }
} }
@@ -1002,16 +1027,26 @@ func (p *Parser) parseMapElementLit() *ast.MapElementLit {
defer un(trace(p, "MapElementLit")) defer un(trace(p, "MapElementLit"))
} }
// key: read identifier token but it's not actually an identifier pos := p.pos
ident := p.parseIdent() name := "_"
if p.token == token.Ident {
name = p.tokenLit
} else if p.token == token.String {
v, _ := strconv.Unquote(p.tokenLit)
name = v
} else {
p.errorExpected(pos, "map key")
}
p.next()
colonPos := p.expect(token.Colon) colonPos := p.expect(token.Colon)
valueExpr := p.parseExpr() valueExpr := p.parseExpr()
return &ast.MapElementLit{ return &ast.MapElementLit{
Key: ident.Name, Key: name,
KeyPos: ident.NamePos, KeyPos: pos,
ColonPos: colonPos, ColonPos: colonPos,
Value: valueExpr, Value: valueExpr,
} }

View File

@@ -6,7 +6,7 @@ type SymbolScope string
// List of symbol scopes // List of symbol scopes
const ( const (
ScopeGlobal SymbolScope = "GLOBAL" ScopeGlobal SymbolScope = "GLOBAL"
ScopeLocal = "LOCAL" ScopeLocal SymbolScope = "LOCAL"
ScopeBuiltin = "BUILTIN" ScopeBuiltin SymbolScope = "BUILTIN"
ScopeFree = "FREE" ScopeFree SymbolScope = "FREE"
) )

View File

@@ -2,12 +2,13 @@ package compiler
// SymbolTable represents a symbol table. // SymbolTable represents a symbol table.
type SymbolTable struct { type SymbolTable struct {
parent *SymbolTable parent *SymbolTable
block bool block bool
store map[string]*Symbol store map[string]*Symbol
numDefinition int numDefinition int
maxDefinition int maxDefinition int
freeSymbols []*Symbol freeSymbols []*Symbol
builtinSymbols []*Symbol
} }
// NewSymbolTable creates a SymbolTable. // NewSymbolTable creates a SymbolTable.
@@ -37,6 +38,10 @@ func (t *SymbolTable) Define(name string) *Symbol {
// DefineBuiltin adds a symbol for builtin function. // DefineBuiltin adds a symbol for builtin function.
func (t *SymbolTable) DefineBuiltin(index int, name string) *Symbol { func (t *SymbolTable) DefineBuiltin(index int, name string) *Symbol {
if t.parent != nil {
return t.parent.DefineBuiltin(index, name)
}
symbol := &Symbol{ symbol := &Symbol{
Name: name, Name: name,
Index: index, Index: index,
@@ -45,6 +50,8 @@ func (t *SymbolTable) DefineBuiltin(index int, name string) *Symbol {
t.store[name] = symbol t.store[name] = symbol
t.builtinSymbols = append(t.builtinSymbols, symbol)
return symbol return symbol
} }
@@ -57,9 +64,7 @@ func (t *SymbolTable) Resolve(name string) (symbol *Symbol, depth int, ok bool)
return return
} }
if !t.block { depth++
depth++
}
// if symbol is defined in parent table and if it's not global/builtin // if symbol is defined in parent table and if it's not global/builtin
// then it's free variable. // then it's free variable.
@@ -101,6 +106,15 @@ func (t *SymbolTable) FreeSymbols() []*Symbol {
return t.freeSymbols return t.freeSymbols
} }
// BuiltinSymbols returns builtin symbols for the scope.
func (t *SymbolTable) BuiltinSymbols() []*Symbol {
if t.parent != nil {
return t.parent.BuiltinSymbols()
}
return t.builtinSymbols
}
// Names returns the name of all the symbols. // Names returns the name of all the symbols.
func (t *SymbolTable) Names() []string { func (t *SymbolTable) Names() []string {
var names []string var names []string

View File

@@ -1,37 +0,0 @@
package objects
import "github.com/d5/tengo/compiler/token"
// Break represents a break statement.
type Break struct{}
// TypeName returns the name of the type.
func (o *Break) TypeName() string {
return "break"
}
func (o *Break) String() string {
return "<break>"
}
// BinaryOp returns another object that is the result of
// a given binary operator and a right-hand side object.
func (o *Break) BinaryOp(op token.Token, rhs Object) (Object, error) {
return nil, ErrInvalidOperator
}
// Copy returns a copy of the type.
func (o *Break) Copy() Object {
return &Break{}
}
// IsFalsy returns true if the value of the type is falsy.
func (o *Break) IsFalsy() bool {
return false
}
// Equals returns true if the value of the type
// is equal to the value of another object.
func (o *Break) Equals(x Object) bool {
return false
}

View File

@@ -1,5 +1,7 @@
package objects package objects
import "github.com/d5/tengo"
func builtinString(args ...Object) (Object, error) { func builtinString(args ...Object) (Object, error) {
argsLen := len(args) argsLen := len(args)
if !(argsLen == 1 || argsLen == 2) { if !(argsLen == 1 || argsLen == 2) {
@@ -12,6 +14,10 @@ func builtinString(args ...Object) (Object, error) {
v, ok := ToString(args[0]) v, ok := ToString(args[0])
if ok { if ok {
if len(v) > tengo.MaxStringLen {
return nil, ErrStringLimit
}
return &String{Value: v}, nil return &String{Value: v}, nil
} }
@@ -117,11 +123,19 @@ func builtinBytes(args ...Object) (Object, error) {
// bytes(N) => create a new bytes with given size N // bytes(N) => create a new bytes with given size N
if n, ok := args[0].(*Int); ok { if n, ok := args[0].(*Int); ok {
if n.Value > int64(tengo.MaxBytesLen) {
return nil, ErrBytesLimit
}
return &Bytes{Value: make([]byte, int(n.Value))}, nil return &Bytes{Value: make([]byte, int(n.Value))}, nil
} }
v, ok := ToByteSlice(args[0]) v, ok := ToByteSlice(args[0])
if ok { if ok {
if len(v) > tengo.MaxBytesLen {
return nil, ErrBytesLimit
}
return &Bytes{Value: v}, nil return &Bytes{Value: v}, nil
} }

27
vendor/github.com/d5/tengo/objects/builtin_format.go generated vendored Normal file
View File

@@ -0,0 +1,27 @@
package objects
func builtinFormat(args ...Object) (Object, error) {
numArgs := len(args)
if numArgs == 0 {
return nil, ErrWrongNumArguments
}
format, ok := args[0].(*String)
if !ok {
return nil, ErrInvalidArgumentType{
Name: "format",
Expected: "string",
Found: args[0].TypeName(),
}
}
if numArgs == 1 {
return format, nil // okay to return 'format' directly as String is immutable
}
s, err := Format(format.Value, args[1:]...)
if err != nil {
return nil, err
}
return &String{Value: s}, nil
}

View File

@@ -1,54 +0,0 @@
package objects
import (
"encoding/json"
)
// to_json(v object) => bytes
func builtinToJSON(args ...Object) (Object, error) {
if len(args) != 1 {
return nil, ErrWrongNumArguments
}
res, err := json.Marshal(objectToInterface(args[0]))
if err != nil {
return &Error{Value: &String{Value: err.Error()}}, nil
}
return &Bytes{Value: res}, nil
}
// from_json(data string/bytes) => object
func builtinFromJSON(args ...Object) (Object, error) {
if len(args) != 1 {
return nil, ErrWrongNumArguments
}
var target interface{}
switch o := args[0].(type) {
case *Bytes:
err := json.Unmarshal(o.Value, &target)
if err != nil {
return &Error{Value: &String{Value: err.Error()}}, nil
}
case *String:
err := json.Unmarshal([]byte(o.Value), &target)
if err != nil {
return &Error{Value: &String{Value: err.Error()}}, nil
}
default:
return nil, ErrInvalidArgumentType{
Name: "first",
Expected: "bytes/string",
Found: args[0].TypeName(),
}
}
res, err := FromInterface(target)
if err != nil {
return nil, err
}
return res, nil
}

23
vendor/github.com/d5/tengo/objects/builtin_module.go generated vendored Normal file
View File

@@ -0,0 +1,23 @@
package objects
// BuiltinModule is an importable module that's written in Go.
type BuiltinModule struct {
Attrs map[string]Object
}
// Import returns an immutable map for the module.
func (m *BuiltinModule) Import(moduleName string) (interface{}, error) {
return m.AsImmutableMap(moduleName), nil
}
// AsImmutableMap converts builtin module into an immutable map.
func (m *BuiltinModule) AsImmutableMap(moduleName string) *ImmutableMap {
attrs := make(map[string]Object, len(m.Attrs))
for k, v := range m.Attrs {
attrs[k] = v.Copy()
}
attrs["__module_name__"] = &String{Value: moduleName}
return &ImmutableMap{Value: attrs}
}

View File

@@ -1,75 +0,0 @@
package objects
import (
"fmt"
)
// print(args...)
func builtinPrint(args ...Object) (Object, error) {
for _, arg := range args {
if str, ok := arg.(*String); ok {
fmt.Println(str.Value)
} else {
fmt.Println(arg.String())
}
}
return nil, nil
}
// printf("format", args...)
func builtinPrintf(args ...Object) (Object, error) {
numArgs := len(args)
if numArgs == 0 {
return nil, ErrWrongNumArguments
}
format, ok := args[0].(*String)
if !ok {
return nil, ErrInvalidArgumentType{
Name: "format",
Expected: "string",
Found: args[0].TypeName(),
}
}
if numArgs == 1 {
fmt.Print(format)
return nil, nil
}
formatArgs := make([]interface{}, numArgs-1, numArgs-1)
for idx, arg := range args[1:] {
formatArgs[idx] = objectToInterface(arg)
}
fmt.Printf(format.Value, formatArgs...)
return nil, nil
}
// sprintf("format", args...)
func builtinSprintf(args ...Object) (Object, error) {
numArgs := len(args)
if numArgs == 0 {
return nil, ErrWrongNumArguments
}
format, ok := args[0].(*String)
if !ok {
return nil, ErrInvalidArgumentType{
Name: "format",
Expected: "string",
Found: args[0].TypeName(),
}
}
if numArgs == 1 {
return format, nil // okay to return 'format' directly as String is immutable
}
formatArgs := make([]interface{}, numArgs-1, numArgs-1)
for idx, arg := range args[1:] {
formatArgs[idx] = objectToInterface(arg)
}
return &String{Value: fmt.Sprintf(format.Value, formatArgs...)}, nil
}

View File

@@ -181,3 +181,15 @@ func builtinIsCallable(args ...Object) (Object, error) {
return FalseValue, nil return FalseValue, nil
} }
func builtinIsIterable(args ...Object) (Object, error) {
if len(args) != 1 {
return nil, ErrWrongNumArguments
}
if _, ok := args[0].(Iterable); ok {
return TrueValue, nil
}
return FalseValue, nil
}

View File

@@ -1,135 +1,118 @@
package objects package objects
// NamedBuiltinFunc is a named builtin function.
type NamedBuiltinFunc struct {
Name string
Func CallableFunc
}
// Builtins contains all default builtin functions. // Builtins contains all default builtin functions.
var Builtins = []NamedBuiltinFunc{ // Use GetBuiltinFunctions instead of accessing Builtins directly.
var Builtins = []*BuiltinFunction{
{ {
Name: "print", Name: "len",
Func: builtinPrint, Value: builtinLen,
}, },
{ {
Name: "printf", Name: "copy",
Func: builtinPrintf, Value: builtinCopy,
}, },
{ {
Name: "sprintf", Name: "append",
Func: builtinSprintf, Value: builtinAppend,
}, },
{ {
Name: "len", Name: "string",
Func: builtinLen, Value: builtinString,
}, },
{ {
Name: "copy", Name: "int",
Func: builtinCopy, Value: builtinInt,
}, },
{ {
Name: "append", Name: "bool",
Func: builtinAppend, Value: builtinBool,
}, },
{ {
Name: "string", Name: "float",
Func: builtinString, Value: builtinFloat,
}, },
{ {
Name: "int", Name: "char",
Func: builtinInt, Value: builtinChar,
}, },
{ {
Name: "bool", Name: "bytes",
Func: builtinBool, Value: builtinBytes,
}, },
{ {
Name: "float", Name: "time",
Func: builtinFloat, Value: builtinTime,
}, },
{ {
Name: "char", Name: "is_int",
Func: builtinChar, Value: builtinIsInt,
}, },
{ {
Name: "bytes", Name: "is_float",
Func: builtinBytes, Value: builtinIsFloat,
}, },
{ {
Name: "time", Name: "is_string",
Func: builtinTime, Value: builtinIsString,
}, },
{ {
Name: "is_int", Name: "is_bool",
Func: builtinIsInt, Value: builtinIsBool,
}, },
{ {
Name: "is_float", Name: "is_char",
Func: builtinIsFloat, Value: builtinIsChar,
}, },
{ {
Name: "is_string", Name: "is_bytes",
Func: builtinIsString, Value: builtinIsBytes,
}, },
{ {
Name: "is_bool", Name: "is_array",
Func: builtinIsBool, Value: builtinIsArray,
}, },
{ {
Name: "is_char", Name: "is_immutable_array",
Func: builtinIsChar, Value: builtinIsImmutableArray,
}, },
{ {
Name: "is_bytes", Name: "is_map",
Func: builtinIsBytes, Value: builtinIsMap,
}, },
{ {
Name: "is_array", Name: "is_immutable_map",
Func: builtinIsArray, Value: builtinIsImmutableMap,
}, },
{ {
Name: "is_immutable_array", Name: "is_iterable",
Func: builtinIsImmutableArray, Value: builtinIsIterable,
}, },
{ {
Name: "is_map", Name: "is_time",
Func: builtinIsMap, Value: builtinIsTime,
}, },
{ {
Name: "is_immutable_map", Name: "is_error",
Func: builtinIsImmutableMap, Value: builtinIsError,
}, },
{ {
Name: "is_time", Name: "is_undefined",
Func: builtinIsTime, Value: builtinIsUndefined,
}, },
{ {
Name: "is_error", Name: "is_function",
Func: builtinIsError, Value: builtinIsFunction,
}, },
{ {
Name: "is_undefined", Name: "is_callable",
Func: builtinIsUndefined, Value: builtinIsCallable,
}, },
{ {
Name: "is_function", Name: "type_name",
Func: builtinIsFunction, Value: builtinTypeName,
}, },
{ {
Name: "is_callable", Name: "format",
Func: builtinIsCallable, Value: builtinFormat,
},
{
Name: "to_json",
Func: builtinToJSON,
},
{
Name: "from_json",
Func: builtinFromJSON,
},
{
Name: "type_name",
Func: builtinTypeName,
}, },
} }

View File

@@ -3,6 +3,7 @@ package objects
import ( import (
"bytes" "bytes"
"github.com/d5/tengo"
"github.com/d5/tengo/compiler/token" "github.com/d5/tengo/compiler/token"
) )
@@ -27,6 +28,10 @@ func (o *Bytes) BinaryOp(op token.Token, rhs Object) (Object, error) {
case token.Add: case token.Add:
switch rhs := rhs.(type) { switch rhs := rhs.(type) {
case *Bytes: case *Bytes:
if len(o.Value)+len(rhs.Value) > tengo.MaxBytesLen {
return nil, ErrBytesLimit
}
return &Bytes{Value: append(o.Value, rhs.Value...)}, nil return &Bytes{Value: append(o.Value, rhs.Value...)}, nil
} }
} }
@@ -52,7 +57,7 @@ func (o *Bytes) Equals(x Object) bool {
return false return false
} }
return bytes.Compare(o.Value, t.Value) == 0 return bytes.Equal(o.Value, t.Value)
} }
// IndexGet returns an element (as Int) at a given index. // IndexGet returns an element (as Int) at a given index.
@@ -74,3 +79,11 @@ func (o *Bytes) IndexGet(index Object) (res Object, err error) {
return return
} }
// Iterate creates a bytes iterator.
func (o *Bytes) Iterate() Iterator {
return &BytesIterator{
v: o.Value,
l: len(o.Value),
}
}

57
vendor/github.com/d5/tengo/objects/bytes_iterator.go generated vendored Normal file
View File

@@ -0,0 +1,57 @@
package objects
import "github.com/d5/tengo/compiler/token"
// BytesIterator represents an iterator for a string.
type BytesIterator struct {
v []byte
i int
l int
}
// TypeName returns the name of the type.
func (i *BytesIterator) TypeName() string {
return "bytes-iterator"
}
func (i *BytesIterator) String() string {
return "<bytes-iterator>"
}
// BinaryOp returns another object that is the result of
// a given binary operator and a right-hand side object.
func (i *BytesIterator) BinaryOp(op token.Token, rhs Object) (Object, error) {
return nil, ErrInvalidOperator
}
// IsFalsy returns true if the value of the type is falsy.
func (i *BytesIterator) IsFalsy() bool {
return true
}
// Equals returns true if the value of the type
// is equal to the value of another object.
func (i *BytesIterator) Equals(Object) bool {
return false
}
// Copy returns a copy of the type.
func (i *BytesIterator) Copy() Object {
return &BytesIterator{v: i.v, i: i.i, l: i.l}
}
// Next returns true if there are more elements to iterate.
func (i *BytesIterator) Next() bool {
i.i++
return i.i <= i.l
}
// Key returns the key or index value of the current element.
func (i *BytesIterator) Key() Object {
return &Int{Value: int64(i.i - 1)}
}
// Value returns the value of the current element.
func (i *BytesIterator) Value() Object {
return &Int{Value: int64(i.v[i.i-1])}
}

View File

@@ -1,4 +1,4 @@
package objects package objects
// CallableFunc is a function signature for the callable functions. // CallableFunc is a function signature for the callable functions.
type CallableFunc func(args ...Object) (ret Object, err error) type CallableFunc = func(args ...Object) (ret Object, err error)

View File

@@ -7,7 +7,7 @@ import (
// Closure represents a function closure. // Closure represents a function closure.
type Closure struct { type Closure struct {
Fn *CompiledFunction Fn *CompiledFunction
Free []*Object Free []*ObjectPtr
} }
// TypeName returns the name of the type. // TypeName returns the name of the type.
@@ -29,7 +29,7 @@ func (o *Closure) BinaryOp(op token.Token, rhs Object) (Object, error) {
func (o *Closure) Copy() Object { func (o *Closure) Copy() Object {
return &Closure{ return &Closure{
Fn: o.Fn.Copy().(*CompiledFunction), Fn: o.Fn.Copy().(*CompiledFunction),
Free: append([]*Object{}, o.Free...), // DO NOT Copy() of elements; these are variable pointers Free: append([]*ObjectPtr{}, o.Free...), // DO NOT Copy() of elements; these are variable pointers
} }
} }

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