Compare commits
229 Commits
v1.12.3
...
discord-we
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
831b5b332f | ||
|
|
d93879bca5 | ||
|
|
79a006c8de | ||
|
|
ff27746c0c | ||
|
|
87788f354f | ||
|
|
7d2e440c83 | ||
|
|
5551f9d56f | ||
|
|
1fb91c6316 | ||
|
|
e60949ff3f | ||
|
|
278a3c6890 | ||
|
|
fcf734eb36 | ||
|
|
cf3cddafab | ||
|
|
c52664f22e | ||
|
|
cb712ff37d | ||
|
|
f4ae610448 | ||
|
|
601b8bc98d | ||
|
|
80b4cec87a | ||
|
|
76c7b69e4e | ||
|
|
a5bd3c4dda | ||
|
|
f06e9b5605 | ||
|
|
7a3bb0e55c | ||
|
|
6e8f535e8b | ||
|
|
5619a75b05 | ||
|
|
53dfb78215 | ||
|
|
8e97cbab1e | ||
|
|
ce7b749fd5 | ||
|
|
6617bd6609 | ||
|
|
e610fb3201 | ||
|
|
40f1d35415 | ||
|
|
b79bf7d414 | ||
|
|
3724cc3a15 | ||
|
|
3418e8c9af | ||
|
|
9619dff334 | ||
|
|
1b2feb19e5 | ||
|
|
1829dc3d9f | ||
|
|
bd0e81f5a0 | ||
|
|
f04d360ee2 | ||
|
|
92f27281fa | ||
|
|
65781b9316 | ||
|
|
9be0be0316 | ||
|
|
9f5f004725 | ||
|
|
fed77cccf3 | ||
|
|
9b520dfb78 | ||
|
|
8ad2be10b2 | ||
|
|
2d277a15f5 | ||
|
|
d60468bb05 | ||
|
|
82d6210464 | ||
|
|
ff198042d2 | ||
|
|
6b47e29583 | ||
|
|
380c38674c | ||
|
|
3c14a0891e | ||
|
|
8513a07416 | ||
|
|
220485a849 | ||
|
|
4db34b0506 | ||
|
|
5677c912a8 | ||
|
|
7a24de15e4 | ||
|
|
99d9ea283a | ||
|
|
dac92a0e0a | ||
|
|
a25efb16f3 | ||
|
|
e4d73b29a1 | ||
|
|
8a875f292e | ||
|
|
60a85621ea | ||
|
|
115d20373c | ||
|
|
cdf33e5748 | ||
|
|
01d0a9f412 | ||
|
|
8cc2d3b4fe | ||
|
|
aba9e4f3be | ||
|
|
4d575ba13a | ||
|
|
7f0e4ad448 | ||
|
|
17cc14a9d2 | ||
|
|
1f8016182c | ||
|
|
caf9ef2c4b | ||
|
|
64b57f2da3 | ||
|
|
efd2c99862 | ||
|
|
cc05ba8907 | ||
|
|
16763b715a | ||
|
|
ffaa598796 | ||
|
|
858e16d34f | ||
|
|
a60e62efb1 | ||
|
|
97f9d4be67 | ||
|
|
fa4eec41f7 | ||
|
|
77516c97db | ||
|
|
cba01f0865 | ||
|
|
8b754017ca | ||
|
|
a27600046e | ||
|
|
fb2667631d | ||
|
|
b638f7037a | ||
|
|
74699a8262 | ||
|
|
eabf2a4582 | ||
|
|
325d62b41c | ||
|
|
e955a056e2 | ||
|
|
723f8c5fd5 | ||
|
|
a16137f53f | ||
|
|
d60b8b97f9 | ||
|
|
7b0bc51183 | ||
|
|
53aa076555 | ||
|
|
f57370f33a | ||
|
|
c557d51b6f | ||
|
|
df3fdc26a0 | ||
|
|
af00c34aac | ||
|
|
120bf39f55 | ||
|
|
26a7e35f27 | ||
|
|
d44d2a5f00 | ||
|
|
7f1d86b338 | ||
|
|
d8816280f0 | ||
|
|
b09a73040f | ||
|
|
740b5f2602 | ||
|
|
96841c70c7 | ||
|
|
f92735d35d | ||
|
|
516fd3c92d | ||
|
|
a775b57134 | ||
|
|
bf21604d42 | ||
|
|
1bb39eba87 | ||
|
|
3190703dc8 | ||
|
|
5095db8a43 | ||
|
|
1f1634ea59 | ||
|
|
a7dd033c3b | ||
|
|
95e78ffa05 | ||
|
|
42276ea7d0 | ||
|
|
dffd67eb31 | ||
|
|
55e79063d6 | ||
|
|
46f4bbb3b5 | ||
|
|
240559581a | ||
|
|
48ba829465 | ||
|
|
eef654de98 | ||
|
|
d76a04bd0a | ||
|
|
a8fe54a78d | ||
|
|
0bcb0b882f | ||
|
|
4525fa31aa | ||
|
|
aeaea0574f | ||
|
|
99d71c2177 | ||
|
|
3e60cfafd3 | ||
|
|
3123695869 | ||
|
|
777af73e2b | ||
|
|
716751cf76 | ||
|
|
6ebd5cbbd8 | ||
|
|
077b818d82 | ||
|
|
5af1d80055 | ||
|
|
f236d12166 | ||
|
|
127eb908f3 | ||
|
|
40d76b2296 | ||
|
|
8147815037 | ||
|
|
57f156be83 | ||
|
|
2cfd880cdb | ||
|
|
430b38e770 | ||
|
|
e7f463a082 | ||
|
|
1d39c771e4 | ||
|
|
c81c0dd22a | ||
|
|
f8a1ab4622 | ||
|
|
5d3309fdcd | ||
|
|
4ae028fe73 | ||
|
|
707db950c8 | ||
|
|
94812d8648 | ||
|
|
8548b69e6e | ||
|
|
e3cb665d92 | ||
|
|
fb713ed91b | ||
|
|
d99eacc2e1 | ||
|
|
62e55214fc | ||
|
|
464d27ad7e | ||
|
|
f88c5f6c08 | ||
|
|
da6ce791bc | ||
|
|
b33b50987b | ||
|
|
5193634a52 | ||
|
|
0d94746f4a | ||
|
|
85680935d4 | ||
|
|
46e2683995 | ||
|
|
492722af8b | ||
|
|
56749dfb20 | ||
|
|
04567c765e | ||
|
|
048158ad6d | ||
|
|
7326b9e10d | ||
|
|
8522d8f29c | ||
|
|
bab385c342 | ||
|
|
bb27ef7939 | ||
|
|
d2044c647b | ||
|
|
c585d00f16 | ||
|
|
015c076315 | ||
|
|
426aa33723 | ||
|
|
da8e415ae1 | ||
|
|
1b834c6858 | ||
|
|
d82726cd1b | ||
|
|
288f0a06bb | ||
|
|
0121d75032 | ||
|
|
53c86702a3 | ||
|
|
192fe89789 | ||
|
|
959ca3cef3 | ||
|
|
ccd55d2a28 | ||
|
|
bfa9a83d31 | ||
|
|
2f7b4d7f68 | ||
|
|
d887855e16 | ||
|
|
1a1e68ec98 | ||
|
|
a2754f15fc | ||
|
|
b6d81f34ba | ||
|
|
f9fb33e696 | ||
|
|
f72d5de2d7 | ||
|
|
0365c0786a | ||
|
|
af7a00d030 | ||
|
|
8a7efce941 | ||
|
|
ce73aa5a74 | ||
|
|
64d63a25cc | ||
|
|
2cef9c4fcf | ||
|
|
4265d43096 | ||
|
|
25857591a2 | ||
|
|
27f5a1a685 | ||
|
|
84da2d6a29 | ||
|
|
859ebad55d | ||
|
|
e538a4d304 | ||
|
|
f94c2b40a3 | ||
|
|
47d29ecf63 | ||
|
|
f2088a687e | ||
|
|
faeeee2948 | ||
|
|
7923cfe8f8 | ||
|
|
b51d0a9b05 | ||
|
|
09f22a801e | ||
|
|
3a824c5f9d | ||
|
|
df02f51c56 | ||
|
|
fc5e3a6728 | ||
|
|
57fbd3c723 | ||
|
|
25cd1e2cc1 | ||
|
|
f5659d455d | ||
|
|
5ed7abdbeb | ||
|
|
09875fe160 | ||
|
|
f716b8fc0f | ||
|
|
9f66f93641 | ||
|
|
f00d4d7d3f | ||
|
|
0929535b2e | ||
|
|
8869e253ca | ||
|
|
f3a5ea2956 | ||
|
|
f4d4dc91b1 |
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Exclude matterbridge binary
|
||||
matterbridge
|
||||
|
||||
# Exclude configuration file
|
||||
matterbridge.toml
|
||||
208
.golangci.yaml
Normal file
208
.golangci.yaml
Normal file
@@ -0,0 +1,208 @@
|
||||
# For full documentation of the configuration options please
|
||||
# see: https://github.com/golangci/golangci-lint#config-file.
|
||||
|
||||
# options for analysis running
|
||||
run:
|
||||
# default concurrency is the available CPU number
|
||||
# concurrency: 4
|
||||
|
||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||
deadline: 2m
|
||||
|
||||
# exit code when at least one issue was found, default is 1
|
||||
issues-exit-code: 1
|
||||
|
||||
# include test files or not, default is true
|
||||
tests: true
|
||||
|
||||
# list of build tags, all linters use it. Default is empty list.
|
||||
build-tags:
|
||||
|
||||
# which dirs to skip: they won't be analyzed;
|
||||
# can use regexp here: generated.*, regexp is applied on full path;
|
||||
# default value is empty list, but next dirs are always skipped independently
|
||||
# from this option's value:
|
||||
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
||||
skip-dirs:
|
||||
|
||||
# which files to skip: they will be analyzed, but issues from them
|
||||
# won't be reported. Default value is empty list, but there is
|
||||
# no need to include all autogenerated files, we confidently recognize
|
||||
# autogenerated files. If it's not please let us know.
|
||||
skip-files:
|
||||
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
|
||||
format: colored-line-number
|
||||
|
||||
# print lines of code with issue, default is true
|
||||
print-issued-lines: true
|
||||
|
||||
# print linter name in the end of issue text, default is true
|
||||
print-linter-name: true
|
||||
|
||||
|
||||
# all available settings of specific linters, we can set an option for
|
||||
# a given linter even if we deactivate that same linter at runtime
|
||||
linters-settings:
|
||||
errcheck:
|
||||
# report about not checking of errors in type assertions: `a := b.(MyStruct)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-type-assertions: false
|
||||
|
||||
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-blank: false
|
||||
govet:
|
||||
# report about shadowed variables
|
||||
check-shadowing: true
|
||||
golint:
|
||||
# minimal confidence for issues, default is 0.8
|
||||
min-confidence: 0.8
|
||||
gofmt:
|
||||
# simplify code: gofmt with `-s` option, true by default
|
||||
simplify: true
|
||||
goimports:
|
||||
# put imports beginning with prefix after 3rd-party packages;
|
||||
# it's a comma-separated list of prefixes
|
||||
local-prefixes: github.com
|
||||
gocyclo:
|
||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||
min-complexity: 15
|
||||
maligned:
|
||||
# print struct with more effective memory layout or not, false by default
|
||||
suggest-new: true
|
||||
dupl:
|
||||
# tokens count to trigger issue, 150 by default
|
||||
threshold: 150
|
||||
goconst:
|
||||
# minimal length of string constant, 3 by default
|
||||
min-len: 3
|
||||
# minimal occurrences count to trigger, 3 by default
|
||||
min-occurrences: 3
|
||||
depguard:
|
||||
list-type: blacklist
|
||||
include-go-root: false
|
||||
packages:
|
||||
# List of packages that we would want to blacklist for... reasons.
|
||||
misspell:
|
||||
# Correct spellings using locale preferences for US or UK.
|
||||
# Default is to use a neutral variety of English.
|
||||
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
||||
locale: US
|
||||
lll:
|
||||
# max line length, lines longer will be reported. Default is 120.
|
||||
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
|
||||
line-length: 150
|
||||
# tab width in spaces. Default to 1.
|
||||
tab-width: 1
|
||||
unused:
|
||||
# treat code as a program (not a library) and report unused exported identifiers; default is false.
|
||||
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
unparam:
|
||||
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
|
||||
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
nakedret:
|
||||
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
|
||||
max-func-lines: 0 # Warn on all naked returns.
|
||||
prealloc:
|
||||
# XXX: we don't recommend using this linter before doing performance profiling.
|
||||
# For most programs usage of prealloc will be a premature optimization.
|
||||
|
||||
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
|
||||
# True by default.
|
||||
simple: true
|
||||
range-loops: true # Report preallocation suggestions on range loops, true by default
|
||||
for-loops: false # Report preallocation suggestions on for loops, false by default
|
||||
gocritic:
|
||||
# which checks should be enabled; can't be combined with 'disabled-checks';
|
||||
# default are: [appendAssign assignOp caseOrder dupArg dupBranchBody dupCase flagDeref
|
||||
# ifElseChain regexpMust singleCaseSwitch sloppyLen switchTrue typeSwitchVar underef
|
||||
# unlambda unslice rangeValCopy defaultCaseOrder];
|
||||
# all checks list: https://github.com/go-critic/checkers
|
||||
# disabled for now - hugeParam
|
||||
enabled-checks:
|
||||
- appendAssign
|
||||
- assignOp
|
||||
- boolExprSimplify
|
||||
- builtinShadow
|
||||
- captLocal
|
||||
- caseOrder
|
||||
- commentedOutImport
|
||||
- defaultCaseOrder
|
||||
- dupArg
|
||||
- dupBranchBody
|
||||
- dupCase
|
||||
- dupSubExpr
|
||||
- elseif
|
||||
- emptyFallthrough
|
||||
- ifElseChain
|
||||
- importShadow
|
||||
- indexAlloc
|
||||
- methodExprCall
|
||||
- nestingReduce
|
||||
- offBy1
|
||||
- ptrToRefParam
|
||||
- regexpMust
|
||||
- singleCaseSwitch
|
||||
- sloppyLen
|
||||
- switchTrue
|
||||
- typeSwitchVar
|
||||
- typeUnparen
|
||||
- underef
|
||||
- unlambda
|
||||
- unnecessaryBlock
|
||||
- unslice
|
||||
- valSwap
|
||||
- wrapperFunc
|
||||
- yodaStyleExpr
|
||||
|
||||
|
||||
# linters that we should / shouldn't run
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- gochecknoglobals
|
||||
- lll
|
||||
- maligned
|
||||
- prealloc
|
||||
|
||||
|
||||
# rules to deal with reported isues
|
||||
issues:
|
||||
# List of regexps of issue texts to exclude, empty list by default.
|
||||
# But independently from this option we use default exclude patterns,
|
||||
# it can be disabled by `exclude-use-default: false`. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`
|
||||
exclude:
|
||||
|
||||
# Independently from option `exclude` we use default exclude patterns,
|
||||
# it can be disabled by this option. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`.
|
||||
# Default value for this option is true.
|
||||
exclude-use-default: true
|
||||
|
||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||
max-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||
max-same-issues: 0
|
||||
|
||||
# Show only new issues: if there are unstaged changes or untracked files,
|
||||
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
|
||||
# It's a super-useful option for integration of golangci-lint into existing
|
||||
# large codebase. It's not practical to fix all existing issues at the moment
|
||||
# of integration: much better don't allow issues in new code.
|
||||
# Default is false.
|
||||
new: false
|
||||
|
||||
# Show only new issues created after git revision `REV`
|
||||
new-from-rev: "HEAD~1"
|
||||
34
.goreleaser.yml
Normal file
34
.goreleaser.yml
Normal 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'
|
||||
73
.travis.yml
73
.travis.yml
@@ -1,55 +1,56 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.11.x
|
||||
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
|
||||
|
||||
git:
|
||||
depth: 200
|
||||
|
||||
env:
|
||||
- GOOS=linux GOARCH=amd64
|
||||
# - GOOS=windows GOARCH=amd64
|
||||
#- GOOS=linux GOARCH=arm
|
||||
|
||||
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:
|
||||
email: false
|
||||
email: false
|
||||
|
||||
before_script:
|
||||
- MY_VERSION=$(git describe --tags)
|
||||
# - GO_FILES=$(find . -iname '*.go' | grep -v /vendor/) # All the .go files, excluding vendor/
|
||||
- PKGS=$(go list ./... | grep -v /vendor/) # All the import paths, excluding vendor/
|
||||
# - go get github.com/golang/lint/golint # Linter
|
||||
#- go get honnef.co/go/tools/cmd/megacheck # Badass static analyzer/linter
|
||||
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.12.2
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /.*/
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: lint
|
||||
# Run linting in one Go environment only.
|
||||
script: ./ci/lint.sh
|
||||
go: 1.12.x
|
||||
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
|
||||
|
||||
# Anything in before_script: that returns a nonzero exit code will
|
||||
# flunk the build and immediately stop. It's sorta like having
|
||||
# set -e enabled in bash.
|
||||
script:
|
||||
#- test -z "$(go fmt ./...)" # Fail if a .go file hasn't been formatted with gofmt
|
||||
- go test -v -race $PKGS # Run all the tests with the race detector enabled
|
||||
#- go vet $PKGS # go vet is the official Go static analyzer
|
||||
- golangci-lint run --enable-all -D lll -D errcheck -D gosec -D maligned -D prealloc -D gocyclo -D gochecknoglobals
|
||||
#- megacheck $PKGS # "go vet on steroids" + linter
|
||||
- /bin/bash ci/bintray.sh
|
||||
#- golint -set_exit_status $PKGS # one last linter
|
||||
before_deploy: /bin/bash ci/bintray.sh
|
||||
|
||||
deploy:
|
||||
on:
|
||||
all_branches: true
|
||||
condition: $BINDEPLOY = 1
|
||||
provider: bintray
|
||||
edge:
|
||||
branch: v1.8.47
|
||||
file: ci/deploy.json
|
||||
user: 42wim
|
||||
key:
|
||||
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="
|
||||
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="
|
||||
|
||||
124
README.md
124
README.md
@@ -5,7 +5,7 @@
|
||||
<br />
|
||||
**A simple chat bridge**<br />
|
||||
Letting people be where they want to be.<br />
|
||||
<sub>Bridges between a growing number of protocols. Click below to demo.</sub>
|
||||
<sub>Bridges between a growing number of protocols. Click below to demo or join the development chat.</sub>
|
||||
|
||||
<sup>
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
[Matrix][mb-matrix] |
|
||||
[Slack][mb-slack] |
|
||||
[Mattermost][mb-mattermost] |
|
||||
[Rocket.Chat][mb-rocketchat] |
|
||||
[XMPP][mb-xmpp] |
|
||||
[Twitch][mb-twitch] |
|
||||
[WhatsApp][mb-whatsapp] |
|
||||
[Zulip][mb-zulip] |
|
||||
[Telegram][mb-telegram] |
|
||||
And more...
|
||||
</sup>
|
||||
|
||||
@@ -32,16 +35,25 @@
|
||||
|
||||
**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
|
||||
* [Features](https://github.com/42wim/matterbridge/wiki/Features)
|
||||
* [Natively supported](#natively-supported)
|
||||
* [3rd party via matterbridge api](#3rd-party-via-matterbridge-api)
|
||||
* [API](#API)
|
||||
* [Requirements](#requirements)
|
||||
* [Chat with us](#chat-with-us)
|
||||
* [Screenshots](https://github.com/42wim/matterbridge/wiki/)
|
||||
* [Installing](#installing)
|
||||
* [Installing/upgrading](#installing--upgrading)
|
||||
* [Binaries](#binaries)
|
||||
* [Building](#building)
|
||||
* [Building](#building)
|
||||
* [Configuration](#configuration)
|
||||
* [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config)
|
||||
* [Settings](#settings)
|
||||
* [Examples](#examples)
|
||||
* [Running](#running)
|
||||
* [Docker](#docker)
|
||||
@@ -61,42 +73,75 @@
|
||||
* [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)
|
||||
* [API](https://github.com/42wim/matterbridge/wiki/Features#api)
|
||||
|
||||
### API
|
||||
The API is very basic at the moment and rather undocumented.
|
||||
### Natively supported
|
||||
|
||||
Used by at least 3 projects. Feel free to make a PR to add your project to this list.
|
||||
|
||||
* [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
|
||||
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
|
||||
* [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
|
||||
|
||||
## Requirements
|
||||
Accounts to one of the supported bridges
|
||||
* [Mattermost](https://github.com/mattermost/platform/) 3.8.x - 3.10.x, 4.x, 5.x
|
||||
* [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x
|
||||
* [IRC](http://www.mirc.com/servers.html)
|
||||
* [XMPP](https://jabber.org)
|
||||
* [XMPP](https://xmpp.org)
|
||||
* [Gitter](https://gitter.im)
|
||||
* [Slack](https://slack.com)
|
||||
* [Discord](https://discordapp.com)
|
||||
* [Telegram](https://telegram.org)
|
||||
* [Hipchat](https://www.hipchat.com)
|
||||
* [Rocket.chat](https://rocket.chat)
|
||||
* [Matrix](https://matrix.org)
|
||||
* [Steam](https://store.steampowered.com/)
|
||||
* [Twitch](https://twitch.tv)
|
||||
* [Ssh-chat](https://github.com/shazow/ssh-chat)
|
||||
* [WhatsApp](https://www.whatsapp.com/)
|
||||
* [Zulip](https://zulipchat.com)
|
||||
|
||||
### 3rd party via matterbridge api
|
||||
* [Minecraft](https://github.com/elytra/MatterLink)
|
||||
* [Reddit](https://github.com/bonehurtingjuice/mattereddit)
|
||||
* [Facebook messenger](https://github.com/VictorNine/fbridge)
|
||||
* [Discourse](https://github.com/DeclanHoare/matterbabble)
|
||||
|
||||
### API
|
||||
The API is basic at the moment.
|
||||
More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api).
|
||||
|
||||
Used by the projects below. Feel free to make a PR to add your project to this list.
|
||||
|
||||
* [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
|
||||
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
|
||||
* [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
|
||||
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
|
||||
* [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
|
||||
|
||||
## Chat with us
|
||||
|
||||
Questions or want to test on your favorite platform? Join below:
|
||||
|
||||
* [Gitter][mb-gitter]
|
||||
* [IRC][mb-irc]
|
||||
* [Discord][mb-discord]
|
||||
* [Matrix][mb-matrix]
|
||||
* [Slack][mb-slack]
|
||||
* [Mattermost][mb-mattermost]
|
||||
* [Rocket.Chat][mb-rocketchat]
|
||||
* [XMPP][mb-xmpp] (matterbridge@conference.jabber.de)
|
||||
* [Twitch][mb-twitch]
|
||||
* [Zulip][mb-zulip]
|
||||
* [Telegram][mb-telegram]
|
||||
|
||||
## Screenshots
|
||||
See https://github.com/42wim/matterbridge/wiki
|
||||
|
||||
## Installing
|
||||
## Installing / upgrading
|
||||
### Binaries
|
||||
* Latest stable release [v1.12.0](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/)
|
||||
|
||||
### Building
|
||||
Go 1.8+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH](https://golang.org/doc/code.html#GOPATH).
|
||||
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
|
||||
* [Overview](https://repology.org/metapackage/matterbridge/versions)
|
||||
|
||||
## 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).
|
||||
|
||||
After Go is setup, download matterbridge to your $GOPATH directory.
|
||||
|
||||
@@ -116,6 +161,9 @@ matterbridge
|
||||
### 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.
|
||||
|
||||
### Settings
|
||||
All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for each bridge.
|
||||
|
||||
### Advanced configuration
|
||||
* [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
|
||||
|
||||
@@ -205,20 +253,21 @@ See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.m
|
||||
|
||||
See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
|
||||
|
||||
Want to tip ?
|
||||
* eth: 0xb3f9b5387c66ad6be892bcb7bbc67862f3abc16f
|
||||
* btc: 1N7cKHj5SfqBHBzDJ6kad4BzeqUBBS2zhs
|
||||
|
||||
## Related projects
|
||||
* [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
|
||||
* [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
|
||||
* [FOSSRIT/infrastructure - roles/matterbridge](https://github.com/FOSSRIT/infrastructure/tree/master/roles/matterbridge) (Ansible role used to automate deployments of Matterbridge)
|
||||
* [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig)
|
||||
* [matterlink](https://github.com/elytra/MatterLink)
|
||||
* [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
|
||||
* [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
|
||||
* [mattereddit](https://github.com/bonehurtingjuice/mattereddit)
|
||||
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
|
||||
* [matterlink](https://github.com/elytra/MatterLink)
|
||||
* [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost
|
||||
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
|
||||
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
|
||||
* [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
|
||||
* [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
|
||||
|
||||
## Articles
|
||||
* [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
|
||||
* https://mattermost.com/blog/connect-irc-to-mattermost/
|
||||
* https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/
|
||||
* https://blog.brightscout.com/top-10-mattermost-integrations/
|
||||
@@ -226,9 +275,16 @@ Want to tip ?
|
||||
* https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/
|
||||
* https://kopano.com/blog/matterbridge-bridging-mattermost-chat/
|
||||
* https://www.stitcher.com/s/?eid=52382713
|
||||
* https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/
|
||||
|
||||
## Thanks
|
||||
[](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:
|
||||
* discord - https://github.com/bwmarrin/discordgo
|
||||
@@ -237,13 +293,16 @@ Matterbridge wouldn't exist without these libraries:
|
||||
* gops - https://github.com/google/gops
|
||||
* gozulipbot - https://github.com/ifo/gozulipbot
|
||||
* irc - https://github.com/lrstanley/girc
|
||||
* mattermost - https://github.com/mattermost/platform
|
||||
* mattermost - https://github.com/mattermost/mattermost-server
|
||||
* matrix - https://github.com/matrix-org/gomatrix
|
||||
* sshchat - https://github.com/shazow/ssh-chat
|
||||
* slack - https://github.com/nlopes/slack
|
||||
* steam - https://github.com/Philipp15b/go-steam
|
||||
* telegram - https://github.com/go-telegram-bot-api/telegram-bot-api
|
||||
* xmpp - https://github.com/mattn/go-xmpp
|
||||
* whatsapp - https://github.com/Rhymen/go-whatsapp/
|
||||
* zulip - https://github.com/ifo/gozulipbot
|
||||
* tengo - https://github.com/d5/tengo
|
||||
|
||||
<!-- Links -->
|
||||
|
||||
@@ -253,6 +312,9 @@ Matterbridge wouldn't exist without these libraries:
|
||||
[mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org
|
||||
[mb-slack]: https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA
|
||||
[mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e
|
||||
[mb-rocketchat]: https://open.rocket.chat/channel/matterbridge
|
||||
[mb-xmpp]: https://inverse.chat/
|
||||
[mb-twitch]: https://www.twitch.tv/matterbridge
|
||||
[mb-whatsapp]: https://www.whatsapp.com/
|
||||
[mb-zulip]: https://matterbridge.zulipchat.com/register/
|
||||
[mb-telegram]: https://t.me/Matterbridge
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/zfjagann/golang-ring"
|
||||
)
|
||||
|
||||
@@ -117,20 +117,14 @@ func (b *API) handleStream(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
c.Response().Flush()
|
||||
closeNotifier := c.Response().CloseNotify()
|
||||
for {
|
||||
select {
|
||||
case <-closeNotifier:
|
||||
return nil
|
||||
default:
|
||||
msg := b.Messages.Dequeue()
|
||||
if msg != nil {
|
||||
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Response().Flush()
|
||||
msg := b.Messages.Dequeue()
|
||||
if msg != nil {
|
||||
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
c.Response().Flush()
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Bridger interface {
|
||||
@@ -16,42 +17,52 @@ type Bridger interface {
|
||||
|
||||
type Bridge struct {
|
||||
Bridger
|
||||
Name string
|
||||
Account string
|
||||
Protocol string
|
||||
Channels map[string]config.ChannelInfo
|
||||
Joined map[string]bool
|
||||
Log *log.Entry
|
||||
Config config.Config
|
||||
General *config.Protocol
|
||||
*sync.RWMutex
|
||||
|
||||
Name string
|
||||
Account string
|
||||
Protocol string
|
||||
Channels map[string]config.ChannelInfo
|
||||
Joined map[string]bool
|
||||
ChannelMembers *config.ChannelMembers
|
||||
Log *logrus.Entry
|
||||
Config config.Config
|
||||
General *config.Protocol
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// General *config.Protocol
|
||||
Remote chan config.Message
|
||||
Log *log.Entry
|
||||
*Bridge
|
||||
|
||||
Remote chan config.Message
|
||||
}
|
||||
|
||||
// Factory is the factory function to create a bridge
|
||||
type Factory func(*Config) Bridger
|
||||
|
||||
func New(bridge *config.Bridge) *Bridge {
|
||||
b := new(Bridge)
|
||||
b.Channels = make(map[string]config.ChannelInfo)
|
||||
accInfo := strings.Split(bridge.Account, ".")
|
||||
protocol := accInfo[0]
|
||||
name := accInfo[1]
|
||||
b.Name = name
|
||||
b.Protocol = protocol
|
||||
b.Account = bridge.Account
|
||||
b.Joined = make(map[string]bool)
|
||||
return b
|
||||
|
||||
return &Bridge{
|
||||
RWMutex: new(sync.RWMutex),
|
||||
Channels: make(map[string]config.ChannelInfo),
|
||||
Name: name,
|
||||
Protocol: protocol,
|
||||
Account: bridge.Account,
|
||||
Joined: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bridge) JoinChannels() error {
|
||||
err := b.joinChannels(b.Channels, b.Joined)
|
||||
return err
|
||||
return b.joinChannels(b.Channels, b.Joined)
|
||||
}
|
||||
|
||||
// SetChannelMembers sets the newMembers to the bridge ChannelMembers
|
||||
func (b *Bridge) SetChannelMembers(newMembers *config.ChannelMembers) {
|
||||
b.Lock()
|
||||
b.ChannelMembers = newMembers
|
||||
b.Unlock()
|
||||
}
|
||||
|
||||
func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
|
||||
|
||||
@@ -8,22 +8,22 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
prefixed "github.com/matterbridge/logrus-prefixed-formatter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
EventJoinLeave = "join_leave"
|
||||
EventTopicChange = "topic_change"
|
||||
EventFailure = "failure"
|
||||
EventFileFailureSize = "file_failure_size"
|
||||
EventAvatarDownload = "avatar_download"
|
||||
EventRejoinChannels = "rejoin_channels"
|
||||
EventUserAction = "user_action"
|
||||
EventMsgDelete = "msg_delete"
|
||||
EventAPIConnected = "api_connected"
|
||||
EventUserTyping = "user_typing"
|
||||
EventJoinLeave = "join_leave"
|
||||
EventTopicChange = "topic_change"
|
||||
EventFailure = "failure"
|
||||
EventFileFailureSize = "file_failure_size"
|
||||
EventAvatarDownload = "avatar_download"
|
||||
EventRejoinChannels = "rejoin_channels"
|
||||
EventUserAction = "user_action"
|
||||
EventMsgDelete = "msg_delete"
|
||||
EventAPIConnected = "api_connected"
|
||||
EventUserTyping = "user_typing"
|
||||
EventGetChannelMembers = "get_channel_members"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
@@ -61,6 +61,16 @@ type ChannelInfo struct {
|
||||
Options ChannelOptions
|
||||
}
|
||||
|
||||
type ChannelMember struct {
|
||||
Username string
|
||||
Nick string
|
||||
UserID string
|
||||
ChannelID string
|
||||
ChannelName string
|
||||
}
|
||||
|
||||
type ChannelMembers []ChannelMember
|
||||
|
||||
type Protocol struct {
|
||||
AuthCode string // steam
|
||||
BindAddress string // mattermost, slack // DEPRECATED
|
||||
@@ -72,6 +82,7 @@ type Protocol struct {
|
||||
EditSuffix string // mattermost, slack, discord, telegram, gitter
|
||||
EditDisable bool // mattermost, slack, discord, telegram, gitter
|
||||
IconURL string // mattermost, slack
|
||||
IgnoreFailureOnStart bool // general
|
||||
IgnoreNicks string // all protocols
|
||||
IgnoreMessages string // all protocols
|
||||
Jid string // xmpp
|
||||
@@ -82,6 +93,7 @@ type Protocol struct {
|
||||
MediaDownloadSize int // all protocols
|
||||
MediaServerDownload string
|
||||
MediaServerUpload string
|
||||
MediaConvertWebPToPNG bool // telegram
|
||||
MessageDelay int // IRC, time in millisecond to wait between messages
|
||||
MessageFormat string // telegram
|
||||
MessageLength int // IRC, max length of a message allowed
|
||||
@@ -108,13 +120,17 @@ type Protocol struct {
|
||||
ReplaceMessages [][]string // all protocols
|
||||
ReplaceNicks [][]string // all protocols
|
||||
RemoteNickFormat string // all protocols
|
||||
RunCommands []string // IRC
|
||||
Server string // IRC,mattermost,XMPP,discord
|
||||
ShowJoinPart bool // all protocols
|
||||
ShowTopicChange bool // slack
|
||||
ShowUserTyping bool // slack
|
||||
ShowEmbeds bool // discord
|
||||
SkipTLSVerify bool // IRC, mattermost
|
||||
SkipVersionCheck bool // mattermost
|
||||
StripNick bool // all protocols
|
||||
SyncTopic bool // slack
|
||||
TengoModifyMessage string // general
|
||||
Team string // mattermost
|
||||
Token string // gitter, slack, discord, api
|
||||
Topic string // zulip
|
||||
@@ -122,17 +138,19 @@ type Protocol struct {
|
||||
UseAPI bool // mattermost, slack
|
||||
UseSASL bool // IRC
|
||||
UseTLS bool // IRC
|
||||
UseDiscriminator bool // discord
|
||||
UseFirstName bool // telegram
|
||||
UseUserName bool // discord
|
||||
UseInsecureURL bool // telegram
|
||||
VerboseJoinPart bool // IRC
|
||||
WebhookBindAddress string // mattermost, slack
|
||||
WebhookURL string // mattermost, slack
|
||||
WebhookUse string // mattermost, slack, discord
|
||||
}
|
||||
|
||||
type ChannelOptions struct {
|
||||
Key string // irc, xmpp
|
||||
WebhookURL string // discord
|
||||
Topic string // zulip
|
||||
}
|
||||
|
||||
type Bridge struct {
|
||||
@@ -150,6 +168,13 @@ type Gateway struct {
|
||||
InOut []Bridge
|
||||
}
|
||||
|
||||
type Tengo struct {
|
||||
InMessage string
|
||||
Message string
|
||||
RemoteNickFormat string
|
||||
OutMessage string
|
||||
}
|
||||
|
||||
type SameChannelGateway struct {
|
||||
Name string
|
||||
Enable bool
|
||||
@@ -171,8 +196,10 @@ type BridgeValues struct {
|
||||
Telegram map[string]Protocol
|
||||
Rocketchat map[string]Protocol
|
||||
SSHChat map[string]Protocol
|
||||
WhatsApp map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results
|
||||
Zulip map[string]Protocol
|
||||
General Protocol
|
||||
Tengo Tengo
|
||||
Gateway []Gateway
|
||||
SameChannelGateway []SameChannelGateway
|
||||
}
|
||||
@@ -187,63 +214,58 @@ type Config interface {
|
||||
}
|
||||
|
||||
type config struct {
|
||||
v *viper.Viper
|
||||
sync.RWMutex
|
||||
|
||||
cv *BridgeValues
|
||||
logger *logrus.Entry
|
||||
v *viper.Viper
|
||||
cv *BridgeValues
|
||||
}
|
||||
|
||||
func NewConfig(cfgfile string) Config {
|
||||
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false})
|
||||
flog := log.WithFields(log.Fields{"prefix": "config"})
|
||||
// NewConfig instantiates a new configuration based on the specified configuration file path.
|
||||
func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
|
||||
logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
|
||||
|
||||
viper.SetConfigFile(cfgfile)
|
||||
input, err := getFileContents(cfgfile)
|
||||
input, err := ioutil.ReadFile(cfgfile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Fatalf("Failed to read configuration file: %#v", err)
|
||||
}
|
||||
mycfg := newConfigFromString(input)
|
||||
|
||||
mycfg := newConfigFromString(logger, input)
|
||||
if mycfg.cv.General.MediaDownloadSize == 0 {
|
||||
mycfg.cv.General.MediaDownloadSize = 1000000
|
||||
}
|
||||
viper.WatchConfig()
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
flog.Println("Config file changed:", e.Name)
|
||||
logger.Println("Config file changed:", e.Name)
|
||||
})
|
||||
return mycfg
|
||||
}
|
||||
|
||||
func getFileContents(filename string) ([]byte, error) {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return []byte(nil), err
|
||||
}
|
||||
return input, nil
|
||||
// NewConfigFromString instantiates a new configuration based on the specified string.
|
||||
func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config {
|
||||
logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
|
||||
return newConfigFromString(logger, input)
|
||||
}
|
||||
|
||||
func NewConfigFromString(input []byte) Config {
|
||||
return newConfigFromString(input)
|
||||
}
|
||||
|
||||
func newConfigFromString(input []byte) *config {
|
||||
func newConfigFromString(logger *logrus.Entry, input []byte) *config {
|
||||
viper.SetConfigType("toml")
|
||||
viper.SetEnvPrefix("matterbridge")
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
|
||||
viper.AutomaticEnv()
|
||||
err := viper.ReadConfig(bytes.NewBuffer(input))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil {
|
||||
logger.Fatalf("Failed to parse the configuration: %s", err)
|
||||
}
|
||||
|
||||
cfg := &BridgeValues{}
|
||||
err = viper.Unmarshal(cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if err := viper.Unmarshal(cfg); err != nil {
|
||||
logger.Fatalf("Failed to load the configuration: %s", err)
|
||||
}
|
||||
return &config{
|
||||
v: viper.GetViper(),
|
||||
cv: cfg,
|
||||
logger: logger,
|
||||
v: viper.GetViper(),
|
||||
cv: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,46 +276,44 @@ func (c *config) BridgeValues() *BridgeValues {
|
||||
func (c *config) GetBool(key string) (bool, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
// log.Debugf("getting bool %s = %#v", key, c.v.GetBool(key))
|
||||
return c.v.GetBool(key), c.v.IsSet(key)
|
||||
}
|
||||
|
||||
func (c *config) GetInt(key string) (int, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
// log.Debugf("getting int %s = %d", key, c.v.GetInt(key))
|
||||
return c.v.GetInt(key), c.v.IsSet(key)
|
||||
}
|
||||
|
||||
func (c *config) GetString(key string) (string, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
// log.Debugf("getting String %s = %s", key, c.v.GetString(key))
|
||||
return c.v.GetString(key), c.v.IsSet(key)
|
||||
}
|
||||
|
||||
func (c *config) GetStringSlice(key string) ([]string, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
// log.Debugf("getting StringSlice %s = %#v", key, c.v.GetStringSlice(key))
|
||||
return c.v.GetStringSlice(key), c.v.IsSet(key)
|
||||
}
|
||||
|
||||
func (c *config) GetStringSlice2D(key string) ([][]string, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
result := [][]string{}
|
||||
if res, ok := c.v.Get(key).([]interface{}); ok {
|
||||
for _, entry := range res {
|
||||
result2 := []string{}
|
||||
for _, entry2 := range entry.([]interface{}) {
|
||||
result2 = append(result2, entry2.(string))
|
||||
}
|
||||
result = append(result, result2)
|
||||
}
|
||||
return result, true
|
||||
|
||||
res, ok := c.v.Get(key).([]interface{})
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return result, false
|
||||
var result [][]string
|
||||
for _, entry := range res {
|
||||
result2 := []string{}
|
||||
for _, entry2 := range entry.([]interface{}) {
|
||||
result2 = append(result2, entry2.(string))
|
||||
}
|
||||
result = append(result, result2)
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
func GetIconURL(msg *Message, iconURL string) string {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -17,18 +16,24 @@ import (
|
||||
const MessageLength = 1950
|
||||
|
||||
type Bdiscord struct {
|
||||
c *discordgo.Session
|
||||
Channels []*discordgo.Channel
|
||||
Nick string
|
||||
UseChannelID bool
|
||||
userMemberMap map[string]*discordgo.Member
|
||||
nickMemberMap map[string]*discordgo.Member
|
||||
guildID string
|
||||
webhookID string
|
||||
webhookToken string
|
||||
channelInfoMap map[string]*config.ChannelInfo
|
||||
sync.RWMutex
|
||||
*bridge.Config
|
||||
|
||||
c *discordgo.Session
|
||||
|
||||
nick string
|
||||
useChannelID bool
|
||||
guildID string
|
||||
webhookID string
|
||||
webhookToken string
|
||||
canEditWebhooks bool
|
||||
|
||||
channelsMutex sync.RWMutex
|
||||
channels []*discordgo.Channel
|
||||
channelInfoMap map[string]*config.ChannelInfo
|
||||
|
||||
membersMutex sync.RWMutex
|
||||
userMemberMap map[string]*discordgo.Member
|
||||
nickMemberMap map[string]*discordgo.Member
|
||||
}
|
||||
|
||||
func New(cfg *bridge.Config) bridge.Bridger {
|
||||
@@ -45,7 +50,8 @@ func New(cfg *bridge.Config) bridge.Bridger {
|
||||
|
||||
func (b *Bdiscord) Connect() error {
|
||||
var err error
|
||||
var token string
|
||||
var guildFound bool
|
||||
token := b.GetString("Token")
|
||||
b.Log.Info("Connecting")
|
||||
if b.GetString("WebhookURL") == "" {
|
||||
b.Log.Info("Connecting using token")
|
||||
@@ -55,6 +61,11 @@ func (b *Bdiscord) Connect() error {
|
||||
if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
|
||||
token = "Bot " + b.GetString("Token")
|
||||
}
|
||||
// if we have a User token, remove the `Bot` prefix
|
||||
if strings.HasPrefix(b.GetString("Token"), "User ") {
|
||||
token = strings.Replace(b.GetString("Token"), "User ", "", -1)
|
||||
}
|
||||
|
||||
b.c, err = discordgo.New(token)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -64,6 +75,9 @@ func (b *Bdiscord) Connect() error {
|
||||
b.c.AddHandler(b.memberUpdate)
|
||||
b.c.AddHandler(b.messageUpdate)
|
||||
b.c.AddHandler(b.messageDelete)
|
||||
b.c.AddHandler(b.messageDeleteBulk)
|
||||
b.c.AddHandler(b.memberAdd)
|
||||
b.c.AddHandler(b.memberRemove)
|
||||
err = b.c.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -76,28 +90,71 @@ func (b *Bdiscord) Connect() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Nick = userinfo.Username
|
||||
serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
|
||||
b.nick = userinfo.Username
|
||||
b.channelsMutex.Lock()
|
||||
for _, guild := range guilds {
|
||||
if guild.Name == b.GetString("Server") {
|
||||
b.Channels, err = b.c.GuildChannels(guild.ID)
|
||||
b.guildID = guild.ID
|
||||
if guild.Name == serverName || guild.ID == serverName {
|
||||
b.channels, err = b.c.GuildChannels(guild.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
break
|
||||
}
|
||||
b.guildID = guild.ID
|
||||
guildFound = true
|
||||
}
|
||||
}
|
||||
for _, channel := range b.Channels {
|
||||
b.Log.Debugf("found channel %#v", channel)
|
||||
b.channelsMutex.Unlock()
|
||||
if !guildFound {
|
||||
msg := fmt.Sprintf("Server \"%s\" not found", b.GetString("Server"))
|
||||
err = errors.New(msg)
|
||||
b.Log.Error(msg)
|
||||
b.Log.Info("Possible values:")
|
||||
for _, guild := range guilds {
|
||||
b.Log.Infof("Server=\"%s\" # Server name", guild.Name)
|
||||
b.Log.Infof("Server=\"%s\" # Server ID", guild.ID)
|
||||
}
|
||||
}
|
||||
// obtaining guild members and initializing nickname mapping
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.channelsMutex.RLock()
|
||||
if b.GetString("WebhookURL") == "" {
|
||||
for _, channel := range b.channels {
|
||||
b.Log.Debugf("found channel %#v", channel)
|
||||
}
|
||||
} else {
|
||||
b.canEditWebhooks = true
|
||||
for _, channel := range b.channels {
|
||||
b.Log.Debugf("found channel %#v; verifying PermissionManageWebhooks", channel)
|
||||
perms, permsErr := b.c.State.UserChannelPermissions(userinfo.ID, channel.ID)
|
||||
manageWebhooks := discordgo.PermissionManageWebhooks
|
||||
if permsErr != nil || perms&manageWebhooks != manageWebhooks {
|
||||
b.Log.Warnf("Can't manage webhooks in channel \"%s\"", channel.Name)
|
||||
b.canEditWebhooks = false
|
||||
}
|
||||
}
|
||||
if b.canEditWebhooks {
|
||||
b.Log.Info("Can manage webhooks; will edit channel for global webhook on send")
|
||||
} else {
|
||||
b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send")
|
||||
}
|
||||
}
|
||||
b.channelsMutex.RUnlock()
|
||||
|
||||
// Obtaining guild members and initializing nickname mapping.
|
||||
b.membersMutex.Lock()
|
||||
defer b.membersMutex.Unlock()
|
||||
members, err := b.c.GuildMembers(b.guildID, "", 1000)
|
||||
if err != nil {
|
||||
b.Log.Error("Error obtaining guild members", err)
|
||||
b.Log.Error("Error obtaining server members: ", err)
|
||||
return err
|
||||
}
|
||||
for _, member := range members {
|
||||
if member == nil {
|
||||
b.Log.Warnf("Skipping missing information for a user.")
|
||||
continue
|
||||
}
|
||||
b.userMemberMap[member.User.ID] = member
|
||||
b.nickMemberMap[member.User.Username] = member
|
||||
if member.Nick != "" {
|
||||
@@ -112,10 +169,13 @@ func (b *Bdiscord) Disconnect() error {
|
||||
}
|
||||
|
||||
func (b *Bdiscord) JoinChannel(channel config.ChannelInfo) error {
|
||||
b.channelsMutex.Lock()
|
||||
defer b.channelsMutex.Unlock()
|
||||
|
||||
b.channelInfoMap[channel.ID] = &channel
|
||||
idcheck := strings.Split(channel.Name, "ID:")
|
||||
if len(idcheck) > 1 {
|
||||
b.UseChannelID = true
|
||||
b.useChannelID = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -133,28 +193,48 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
|
||||
msg.Text = "_" + msg.Text + "_"
|
||||
}
|
||||
|
||||
// use initial webhook
|
||||
// use initial webhook configured for the entire Discord account
|
||||
isGlobalWebhook := true
|
||||
wID := b.webhookID
|
||||
wToken := b.webhookToken
|
||||
|
||||
// check if have a channel specific webhook
|
||||
b.channelsMutex.RLock()
|
||||
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
|
||||
if ci.Options.WebhookURL != "" {
|
||||
wID, wToken = b.splitURL(ci.Options.WebhookURL)
|
||||
isGlobalWebhook = false
|
||||
}
|
||||
}
|
||||
b.channelsMutex.RUnlock()
|
||||
|
||||
// Use webhook to send the message
|
||||
if wID != "" {
|
||||
if wID != "" && msg.Event != config.EventMsgDelete {
|
||||
// skip events
|
||||
if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
|
||||
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")
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text += " " + fi.URL
|
||||
msg.Text = fi.URL
|
||||
if fi.Comment != "" {
|
||||
msg.Text = fi.Comment + ": " + fi.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
// skip empty messages
|
||||
@@ -168,16 +248,24 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
|
||||
if len(msg.Username) > 32 {
|
||||
msg.Username = msg.Username[0:32]
|
||||
}
|
||||
err := b.c.WebhookExecute(
|
||||
wID,
|
||||
wToken,
|
||||
true,
|
||||
&discordgo.WebhookParams{
|
||||
Content: msg.Text,
|
||||
Username: msg.Username,
|
||||
AvatarURL: msg.Avatar,
|
||||
})
|
||||
return "", err
|
||||
// if we have a global webhook for this Discord account, and permission
|
||||
// to modify webhooks (previously verified), then set its channel to
|
||||
// the message channel before using it
|
||||
// TODO: this isn't necessary if the last message from this webhook was
|
||||
// sent to the current channel
|
||||
if isGlobalWebhook && b.canEditWebhooks {
|
||||
b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel)
|
||||
_, err := b.c.WebhookEdit(wID, "", "", channelID)
|
||||
if err != nil {
|
||||
b.Log.Errorf("Could not set webhook channel: %s", err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
msg, err := b.webhookSend(&msg, wID, wToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return msg.ID, nil
|
||||
}
|
||||
|
||||
b.Log.Debugf("Broadcasting using token (API)")
|
||||
@@ -195,7 +283,9 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength)
|
||||
b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text)
|
||||
if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
|
||||
b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
|
||||
}
|
||||
}
|
||||
// check if we have files to upload (from slack, telegram or mattermost)
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
@@ -220,246 +310,15 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
|
||||
return res.ID, err
|
||||
}
|
||||
|
||||
func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) {
|
||||
rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
|
||||
rmsg.Channel = b.getChannelName(m.ChannelID)
|
||||
if b.UseChannelID {
|
||||
rmsg.Channel = "ID:" + m.ChannelID
|
||||
}
|
||||
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
|
||||
if b.GetBool("EditDisable") {
|
||||
return
|
||||
}
|
||||
// only when message is actually edited
|
||||
if m.Message.EditedTimestamp != "" {
|
||||
b.Log.Debugf("Sending edit message")
|
||||
m.Content += b.GetString("EditSuffix")
|
||||
b.messageCreate(s, (*discordgo.MessageCreate)(m))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
var err error
|
||||
|
||||
// not relay our own messages
|
||||
if m.Author.Username == b.Nick {
|
||||
return
|
||||
}
|
||||
// if using webhooks, do not relay if it's ours
|
||||
if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) {
|
||||
return
|
||||
}
|
||||
|
||||
// add the url of the attachments to content
|
||||
if len(m.Attachments) > 0 {
|
||||
for _, attach := range m.Attachments {
|
||||
m.Content = m.Content + "\n" + attach.URL
|
||||
}
|
||||
}
|
||||
|
||||
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
|
||||
|
||||
if m.Content != "" {
|
||||
b.Log.Debugf("== Receiving event %#v", m.Message)
|
||||
m.Message.Content = b.stripCustomoji(m.Message.Content)
|
||||
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
|
||||
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
|
||||
if err != nil {
|
||||
b.Log.Errorf("ContentWithMoreMentionsReplaced failed: %s", err)
|
||||
rmsg.Text = m.ContentWithMentionsReplaced()
|
||||
}
|
||||
}
|
||||
|
||||
// set channel name
|
||||
rmsg.Channel = b.getChannelName(m.ChannelID)
|
||||
if b.UseChannelID {
|
||||
rmsg.Channel = "ID:" + m.ChannelID
|
||||
}
|
||||
|
||||
// set username
|
||||
if !b.GetBool("UseUserName") {
|
||||
rmsg.Username = b.getNick(m.Author)
|
||||
} else {
|
||||
rmsg.Username = m.Author.Username
|
||||
}
|
||||
|
||||
// if we have embedded content add it to text
|
||||
if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil {
|
||||
for _, embed := range m.Message.Embeds {
|
||||
rmsg.Text = rmsg.Text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// no empty messages
|
||||
if rmsg.Text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// do we have a /me action
|
||||
var ok bool
|
||||
rmsg.Text, ok = b.replaceAction(rmsg.Text)
|
||||
if ok {
|
||||
rmsg.Event = config.EventUserAction
|
||||
}
|
||||
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
|
||||
b.Lock()
|
||||
if _, ok := b.userMemberMap[m.Member.User.ID]; ok {
|
||||
b.Log.Debugf("%s: memberupdate: user %s (nick %s) changes nick to %s", b.Account, m.Member.User.Username, b.userMemberMap[m.Member.User.ID].Nick, m.Member.Nick)
|
||||
}
|
||||
b.userMemberMap[m.Member.User.ID] = m.Member
|
||||
b.nickMemberMap[m.Member.User.Username] = m.Member
|
||||
if m.Member.Nick != "" {
|
||||
b.nickMemberMap[m.Member.Nick] = m.Member
|
||||
}
|
||||
b.Unlock()
|
||||
}
|
||||
|
||||
func (b *Bdiscord) getNick(user *discordgo.User) string {
|
||||
var err error
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
if _, ok := b.userMemberMap[user.ID]; ok {
|
||||
if b.userMemberMap[user.ID] != nil {
|
||||
if b.userMemberMap[user.ID].Nick != "" {
|
||||
// only return if nick is set
|
||||
return b.userMemberMap[user.ID].Nick
|
||||
}
|
||||
// otherwise return username
|
||||
return user.Username
|
||||
}
|
||||
}
|
||||
// if we didn't find nick, search for it
|
||||
member, err := b.c.GuildMember(b.guildID, user.ID)
|
||||
if err != nil {
|
||||
return user.Username
|
||||
}
|
||||
b.userMemberMap[user.ID] = member
|
||||
// only return if nick is set
|
||||
if b.userMemberMap[user.ID].Nick != "" {
|
||||
return b.userMemberMap[user.ID].Nick
|
||||
}
|
||||
return user.Username
|
||||
}
|
||||
|
||||
func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
if _, ok := b.nickMemberMap[nick]; ok {
|
||||
if b.nickMemberMap[nick] != nil {
|
||||
return b.nickMemberMap[nick], nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
|
||||
}
|
||||
|
||||
func (b *Bdiscord) getChannelID(name string) string {
|
||||
idcheck := strings.Split(name, "ID:")
|
||||
if len(idcheck) > 1 {
|
||||
return idcheck[1]
|
||||
}
|
||||
for _, channel := range b.Channels {
|
||||
if channel.Name == name {
|
||||
return channel.ID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *Bdiscord) getChannelName(id string) string {
|
||||
for _, channel := range b.Channels {
|
||||
if channel.ID == id {
|
||||
return channel.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *Bdiscord) replaceChannelMentions(text string) string {
|
||||
var err error
|
||||
re := regexp.MustCompile("<#[0-9]+>")
|
||||
text = re.ReplaceAllStringFunc(text, func(m string) string {
|
||||
channel := b.getChannelName(m[2 : len(m)-1])
|
||||
// if at first don't succeed, try again
|
||||
if channel == "" {
|
||||
b.Channels, err = b.c.GuildChannels(b.guildID)
|
||||
if err != nil {
|
||||
return "#unknownchannel"
|
||||
}
|
||||
channel = b.getChannelName(m[2 : len(m)-1])
|
||||
return "#" + channel
|
||||
}
|
||||
return "#" + channel
|
||||
})
|
||||
return text
|
||||
}
|
||||
|
||||
func (b *Bdiscord) replaceUserMentions(text string) string {
|
||||
re := regexp.MustCompile("@[^@]{1,32}")
|
||||
text = re.ReplaceAllStringFunc(text, func(m string) string {
|
||||
mention := strings.TrimSpace(m[1:])
|
||||
var member *discordgo.Member
|
||||
var err error
|
||||
for {
|
||||
b.Log.Debugf("Testing mention: '%s'", mention)
|
||||
member, err = b.getGuildMemberByNick(mention)
|
||||
if err != nil {
|
||||
lastSpace := strings.LastIndex(mention, " ")
|
||||
if lastSpace == -1 {
|
||||
break
|
||||
}
|
||||
mention = strings.TrimSpace(mention[0:lastSpace])
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return m
|
||||
}
|
||||
return strings.Replace(m, "@"+mention, member.User.Mention(), -1)
|
||||
})
|
||||
b.Log.Debugf("Message with mention replaced: %s", text)
|
||||
return text
|
||||
}
|
||||
|
||||
func (b *Bdiscord) replaceAction(text string) (string, bool) {
|
||||
if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") {
|
||||
return strings.Replace(text, "_", "", -1), true
|
||||
}
|
||||
return text, false
|
||||
}
|
||||
|
||||
func (b *Bdiscord) stripCustomoji(text string) string {
|
||||
// <:doge:302803592035958784>
|
||||
re := regexp.MustCompile("<(:.*?:)[0-9]+>")
|
||||
return re.ReplaceAllString(text, `$1`)
|
||||
}
|
||||
|
||||
// splitURL splits a webhookURL and returns the id and token
|
||||
func (b *Bdiscord) splitURL(url string) (string, string) {
|
||||
webhookURLSplit := strings.Split(url, "/")
|
||||
if len(webhookURLSplit) != 7 {
|
||||
b.Log.Fatalf("%s is no correct discord WebhookURL", url)
|
||||
}
|
||||
return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1]
|
||||
}
|
||||
|
||||
// useWebhook returns true if we have a webhook defined somewhere
|
||||
func (b *Bdiscord) useWebhook() bool {
|
||||
if b.GetString("WebhookURL") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
b.channelsMutex.RLock()
|
||||
defer b.channelsMutex.RUnlock()
|
||||
|
||||
for _, channel := range b.channelInfoMap {
|
||||
if channel.Options.WebhookURL != "" {
|
||||
return true
|
||||
@@ -476,6 +335,10 @@ func (b *Bdiscord) isWebhookID(id string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
b.channelsMutex.RLock()
|
||||
defer b.channelsMutex.RUnlock()
|
||||
|
||||
for _, channel := range b.channelInfoMap {
|
||||
if channel.Options.WebhookURL != "" {
|
||||
wID, _ := b.splitURL(channel.Options.WebhookURL)
|
||||
@@ -503,8 +366,56 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
|
||||
}
|
||||
_, err = b.c.ChannelMessageSendComplex(channelID, &m)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("file upload failed: %#v", err)
|
||||
return "", fmt.Errorf("file upload failed: %s", err)
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// webhookSend send one or more message via webhook, taking care of file
|
||||
// uploads (from slack, telegram or mattermost).
|
||||
// Returns messageID and error.
|
||||
func (b *Bdiscord) webhookSend(msg *config.Message, webhookID, token string) (*discordgo.Message, error) {
|
||||
var err error
|
||||
|
||||
// WebhookParams can have either `Content` or `File`.
|
||||
res, err := b.c.WebhookExecute(
|
||||
webhookID,
|
||||
token,
|
||||
true,
|
||||
&discordgo.WebhookParams{
|
||||
Content: msg.Text,
|
||||
Username: msg.Username,
|
||||
AvatarURL: msg.Avatar,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if msg.Extra != nil {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
file := discordgo.File{
|
||||
Name: fi.Name,
|
||||
ContentType: "",
|
||||
Reader: bytes.NewReader(*fi.Data),
|
||||
}
|
||||
_, err := b.c.WebhookExecute(
|
||||
webhookID,
|
||||
token,
|
||||
false,
|
||||
&discordgo.WebhookParams{
|
||||
Username: msg.Username,
|
||||
AvatarURL: msg.Avatar,
|
||||
File: &file,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("file upload failed: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
191
bridge/discord/handlers.go
Normal file
191
bridge/discord/handlers.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package bdiscord
|
||||
|
||||
import (
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam
|
||||
rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
|
||||
rmsg.Channel = b.getChannelName(m.ChannelID)
|
||||
if b.useChannelID {
|
||||
rmsg.Channel = "ID:" + m.ChannelID
|
||||
}
|
||||
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
// 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
|
||||
if b.GetBool("EditDisable") {
|
||||
return
|
||||
}
|
||||
// only when message is actually edited
|
||||
if m.Message.EditedTimestamp != "" {
|
||||
b.Log.Debugf("Sending edit message")
|
||||
m.Content += b.GetString("EditSuffix")
|
||||
b.messageCreate(s, (*discordgo.MessageCreate)(m))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam
|
||||
var err error
|
||||
|
||||
// not relay our own messages
|
||||
if m.Author.Username == b.nick {
|
||||
return
|
||||
}
|
||||
// if using webhooks, do not relay if it's ours
|
||||
if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) {
|
||||
return
|
||||
}
|
||||
|
||||
// add the url of the attachments to content
|
||||
if len(m.Attachments) > 0 {
|
||||
for _, attach := range m.Attachments {
|
||||
m.Content = m.Content + "\n" + attach.URL
|
||||
}
|
||||
}
|
||||
|
||||
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
|
||||
|
||||
if m.Content != "" {
|
||||
b.Log.Debugf("== Receiving event %#v", m.Message)
|
||||
m.Message.Content = b.stripCustomoji(m.Message.Content)
|
||||
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
|
||||
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
|
||||
if err != nil {
|
||||
b.Log.Errorf("ContentWithMoreMentionsReplaced failed: %s", err)
|
||||
rmsg.Text = m.ContentWithMentionsReplaced()
|
||||
}
|
||||
}
|
||||
|
||||
// set channel name
|
||||
rmsg.Channel = b.getChannelName(m.ChannelID)
|
||||
if b.useChannelID {
|
||||
rmsg.Channel = "ID:" + m.ChannelID
|
||||
}
|
||||
|
||||
// set username
|
||||
if !b.GetBool("UseUserName") {
|
||||
rmsg.Username = b.getNick(m.Author)
|
||||
} else {
|
||||
rmsg.Username = m.Author.Username
|
||||
if b.GetBool("UseDiscriminator") {
|
||||
rmsg.Username += "#" + m.Author.Discriminator
|
||||
}
|
||||
}
|
||||
|
||||
// if we have embedded content add it to text
|
||||
if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil {
|
||||
for _, embed := range m.Message.Embeds {
|
||||
rmsg.Text = rmsg.Text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// no empty messages
|
||||
if rmsg.Text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// do we have a /me action
|
||||
var ok bool
|
||||
rmsg.Text, ok = b.replaceAction(rmsg.Text)
|
||||
if ok {
|
||||
rmsg.Event = config.EventUserAction
|
||||
}
|
||||
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
|
||||
if m.Member == nil {
|
||||
b.Log.Warnf("Received member update with no member information: %#v", m)
|
||||
}
|
||||
|
||||
b.membersMutex.Lock()
|
||||
defer b.membersMutex.Unlock()
|
||||
|
||||
if currMember, ok := b.userMemberMap[m.Member.User.ID]; ok {
|
||||
b.Log.Debugf(
|
||||
"%s: memberupdate: user %s (nick %s) changes nick to %s",
|
||||
b.Account,
|
||||
m.Member.User.Username,
|
||||
b.userMemberMap[m.Member.User.ID].Nick,
|
||||
m.Member.Nick,
|
||||
)
|
||||
delete(b.nickMemberMap, currMember.User.Username)
|
||||
delete(b.nickMemberMap, currMember.Nick)
|
||||
delete(b.userMemberMap, m.Member.User.ID)
|
||||
}
|
||||
b.userMemberMap[m.Member.User.ID] = m.Member
|
||||
b.nickMemberMap[m.Member.User.Username] = m.Member
|
||||
if m.Member.Nick != "" {
|
||||
b.nickMemberMap[m.Member.Nick] = m.Member
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
|
||||
if m.Member == nil {
|
||||
b.Log.Warnf("Received member update with no member information: %#v", m)
|
||||
return
|
||||
}
|
||||
username := m.Member.User.Username
|
||||
if m.Member.Nick != "" {
|
||||
username = m.Member.Nick
|
||||
}
|
||||
|
||||
rmsg := config.Message{
|
||||
Account: b.Account,
|
||||
Event: config.EventJoinLeave,
|
||||
Username: "system",
|
||||
Text: username + " joins",
|
||||
}
|
||||
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
|
||||
if m.Member == nil {
|
||||
b.Log.Warnf("Received member update with no member information: %#v", m)
|
||||
return
|
||||
}
|
||||
username := m.Member.User.Username
|
||||
if m.Member.Nick != "" {
|
||||
username = m.Member.Nick
|
||||
}
|
||||
|
||||
rmsg := config.Message{
|
||||
Account: b.Account,
|
||||
Event: config.EventJoinLeave,
|
||||
Username: "system",
|
||||
Text: username + " leaves",
|
||||
}
|
||||
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
237
bridge/discord/helpers.go
Normal file
237
bridge/discord/helpers.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package bdiscord
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func (b *Bdiscord) getNick(user *discordgo.User) string {
|
||||
b.membersMutex.RLock()
|
||||
defer b.membersMutex.RUnlock()
|
||||
|
||||
if member, ok := b.userMemberMap[user.ID]; ok {
|
||||
if member.Nick != "" {
|
||||
// Only return if nick is set.
|
||||
return member.Nick
|
||||
}
|
||||
// Otherwise return username.
|
||||
return user.Username
|
||||
}
|
||||
|
||||
// If we didn't find nick, search for it.
|
||||
member, err := b.c.GuildMember(b.guildID, user.ID)
|
||||
if err != nil {
|
||||
b.Log.Warnf("Failed to fetch information for member %#v: %s", user, err)
|
||||
return user.Username
|
||||
} else if member == nil {
|
||||
b.Log.Warnf("Got no information for member %#v", user)
|
||||
return user.Username
|
||||
}
|
||||
b.userMemberMap[user.ID] = member
|
||||
b.nickMemberMap[member.User.Username] = member
|
||||
if member.Nick != "" {
|
||||
b.nickMemberMap[member.Nick] = member
|
||||
return member.Nick
|
||||
}
|
||||
return user.Username
|
||||
}
|
||||
|
||||
func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error) {
|
||||
b.membersMutex.RLock()
|
||||
defer b.membersMutex.RUnlock()
|
||||
|
||||
if member, ok := b.nickMemberMap[nick]; ok {
|
||||
return member, nil
|
||||
}
|
||||
return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
|
||||
}
|
||||
|
||||
func (b *Bdiscord) getChannelID(name string) string {
|
||||
if strings.Contains(name, "/") {
|
||||
return b.getCategoryChannelID(name)
|
||||
}
|
||||
b.channelsMutex.RLock()
|
||||
defer b.channelsMutex.RUnlock()
|
||||
|
||||
idcheck := strings.Split(name, "ID:")
|
||||
if len(idcheck) > 1 {
|
||||
return idcheck[1]
|
||||
}
|
||||
for _, channel := range b.channels {
|
||||
if channel.Name == name && channel.Type == discordgo.ChannelTypeGuildText {
|
||||
return channel.ID
|
||||
}
|
||||
}
|
||||
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 {
|
||||
b.channelsMutex.RLock()
|
||||
defer b.channelsMutex.RUnlock()
|
||||
|
||||
for _, channel := range b.channels {
|
||||
if channel.ID == id {
|
||||
return b.getCategoryChannelName(channel.Name, channel.ParentID)
|
||||
}
|
||||
}
|
||||
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 (
|
||||
// See https://discordapp.com/developers/docs/reference#message-formatting.
|
||||
channelMentionRE = regexp.MustCompile("<#[0-9]+>")
|
||||
emojiRE = regexp.MustCompile("<(:.*?:)[0-9]+>")
|
||||
userMentionRE = regexp.MustCompile("@[^@\n]{1,32}")
|
||||
)
|
||||
|
||||
func (b *Bdiscord) replaceChannelMentions(text string) string {
|
||||
replaceChannelMentionFunc := func(match string) string {
|
||||
channelID := match[2 : len(match)-1]
|
||||
channelName := b.getChannelName(channelID)
|
||||
|
||||
// If we don't have the channel refresh our list.
|
||||
if channelName == "" {
|
||||
var err error
|
||||
b.channels, err = b.c.GuildChannels(b.guildID)
|
||||
if err != nil {
|
||||
return "#unknownchannel"
|
||||
}
|
||||
channelName = b.getChannelName(channelID)
|
||||
}
|
||||
return "#" + channelName
|
||||
}
|
||||
return channelMentionRE.ReplaceAllStringFunc(text, replaceChannelMentionFunc)
|
||||
}
|
||||
|
||||
func (b *Bdiscord) replaceUserMentions(text string) string {
|
||||
replaceUserMentionFunc := func(match string) string {
|
||||
var (
|
||||
err error
|
||||
member *discordgo.Member
|
||||
username string
|
||||
)
|
||||
|
||||
usernames := enumerateUsernames(match[1:])
|
||||
for _, username = range usernames {
|
||||
b.Log.Debugf("Testing mention: '%s'", username)
|
||||
member, err = b.getGuildMemberByNick(username)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if member == nil {
|
||||
return match
|
||||
}
|
||||
return strings.Replace(match, "@"+username, member.User.Mention(), 1)
|
||||
}
|
||||
return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc)
|
||||
}
|
||||
|
||||
func (b *Bdiscord) stripCustomoji(text string) string {
|
||||
return emojiRE.ReplaceAllString(text, `$1`)
|
||||
}
|
||||
|
||||
func (b *Bdiscord) replaceAction(text string) (string, bool) {
|
||||
if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") {
|
||||
return text[1 : len(text)-1], true
|
||||
}
|
||||
return text, false
|
||||
}
|
||||
|
||||
// splitURL splits a webhookURL and returns the ID and token.
|
||||
func (b *Bdiscord) splitURL(url string) (string, string) {
|
||||
const (
|
||||
expectedWebhookSplitCount = 7
|
||||
webhookIdxID = 5
|
||||
webhookIdxToken = 6
|
||||
)
|
||||
webhookURLSplit := strings.Split(url, "/")
|
||||
if len(webhookURLSplit) != expectedWebhookSplitCount {
|
||||
b.Log.Fatalf("%s is no correct discord WebhookURL", url)
|
||||
}
|
||||
return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken]
|
||||
}
|
||||
|
||||
func enumerateUsernames(s string) []string {
|
||||
onlySpace := true
|
||||
for _, r := range s {
|
||||
if !unicode.IsSpace(r) {
|
||||
onlySpace = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if onlySpace {
|
||||
return nil
|
||||
}
|
||||
|
||||
var username, endSpace string
|
||||
var usernames []string
|
||||
skippingSpace := true
|
||||
for _, r := range s {
|
||||
if unicode.IsSpace(r) {
|
||||
if !skippingSpace {
|
||||
usernames = append(usernames, username)
|
||||
skippingSpace = true
|
||||
}
|
||||
endSpace += string(r)
|
||||
username += string(r)
|
||||
} else {
|
||||
endSpace = ""
|
||||
username += string(r)
|
||||
skippingSpace = false
|
||||
}
|
||||
}
|
||||
if endSpace == "" {
|
||||
usernames = append(usernames, username)
|
||||
}
|
||||
return usernames
|
||||
}
|
||||
46
bridge/discord/helpers_test.go
Normal file
46
bridge/discord/helpers_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package bdiscord
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEnumerateUsernames(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
match string
|
||||
expectedUsernames []string
|
||||
}{
|
||||
"only space": {
|
||||
match: " \t\n \t",
|
||||
expectedUsernames: nil,
|
||||
},
|
||||
"single word": {
|
||||
match: "veni",
|
||||
expectedUsernames: []string{"veni"},
|
||||
},
|
||||
"single word with preceeding space": {
|
||||
match: " vidi",
|
||||
expectedUsernames: []string{" vidi"},
|
||||
},
|
||||
"single word with suffixed space": {
|
||||
match: "vici ",
|
||||
expectedUsernames: []string{"vici"},
|
||||
},
|
||||
"multi-word with varying whitespace": {
|
||||
match: "just me and\tmy friends \t",
|
||||
expectedUsernames: []string{
|
||||
"just",
|
||||
"just me",
|
||||
"just me and",
|
||||
"just me and\tmy",
|
||||
"just me and\tmy friends",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testname, testcase := range testcases {
|
||||
foundUsernames := enumerateUsernames(testcase.match)
|
||||
assert.Equalf(t, testcase.expectedUsernames, foundUsernames, "Should have found the expected usernames for testcase %s", testname)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package helper
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@@ -10,14 +11,19 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/image/webp"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gitlab.com/golang-commonmark/markdown"
|
||||
)
|
||||
|
||||
// DownloadFile downloads the given non-authenticated URL.
|
||||
func DownloadFile(url string) (*[]byte, error) {
|
||||
return DownloadFileAuth(url, "")
|
||||
}
|
||||
|
||||
// DownloadFileAuth downloads the given URL using the specified authentication token.
|
||||
func DownloadFileAuth(url string, auth string) (*[]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
client := &http.Client{
|
||||
@@ -41,8 +47,8 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) {
|
||||
}
|
||||
|
||||
// GetSubLines splits messages in newline-delimited lines. If maxLineLength is
|
||||
// specified as non-zero GetSubLines will and also clip long lines to the
|
||||
// maximum length and insert a warning marker that the line was clipped.
|
||||
// specified as non-zero GetSubLines will also clip long lines to the maximum
|
||||
// length and insert a warning marker that the line was clipped.
|
||||
//
|
||||
// TODO: The current implementation has the inconvenient that it disregards
|
||||
// word boundaries when splitting but this is hard to solve without potentially
|
||||
@@ -78,18 +84,24 @@ func GetSubLines(message string, maxLineLength int) []string {
|
||||
return lines
|
||||
}
|
||||
|
||||
// handle all the stuff we put into extra
|
||||
// HandleExtra manages the supplementary details stored inside a message's 'Extra' field map.
|
||||
func HandleExtra(msg *config.Message, general *config.Protocol) []config.Message {
|
||||
extra := msg.Extra
|
||||
rmsg := []config.Message{}
|
||||
for _, f := range extra[config.EventFileFailureSize] {
|
||||
fi := f.(config.FileInfo)
|
||||
text := fmt.Sprintf("file %s too big to download (%#v > allowed size: %#v)", fi.Name, fi.Size, general.MediaDownloadSize)
|
||||
rmsg = append(rmsg, config.Message{Text: text, Username: "<system> ", Channel: msg.Channel, Account: msg.Account})
|
||||
rmsg = append(rmsg, config.Message{
|
||||
Text: text,
|
||||
Username: "<system> ",
|
||||
Channel: msg.Channel,
|
||||
Account: msg.Account,
|
||||
})
|
||||
}
|
||||
return rmsg
|
||||
}
|
||||
|
||||
// GetAvatar constructs a URL for a given user-avatar if it is available in the cache.
|
||||
func GetAvatar(av map[string]string, userid string, general *config.Protocol) string {
|
||||
if sha, ok := av[userid]; ok {
|
||||
return general.MediaServerDownload + "/" + sha + "/" + userid + ".png"
|
||||
@@ -97,13 +109,15 @@ func GetAvatar(av map[string]string, userid string, general *config.Protocol) st
|
||||
return ""
|
||||
}
|
||||
|
||||
func HandleDownloadSize(flog *log.Entry, msg *config.Message, name string, size int64, general *config.Protocol) error {
|
||||
// HandleDownloadSize checks a specified filename against the configured download blacklist
|
||||
// and checks a specified file-size against the configure limit.
|
||||
func HandleDownloadSize(logger *logrus.Entry, msg *config.Message, name string, size int64, general *config.Protocol) error {
|
||||
// check blacklist here
|
||||
for _, entry := range general.MediaDownloadBlackList {
|
||||
if entry != "" {
|
||||
re, err := regexp.Compile(entry)
|
||||
if err != nil {
|
||||
flog.Errorf("incorrect regexp %s for %s", entry, msg.Account)
|
||||
logger.Errorf("incorrect regexp %s for %s", entry, msg.Account)
|
||||
continue
|
||||
}
|
||||
if re.MatchString(name) {
|
||||
@@ -111,43 +125,77 @@ func HandleDownloadSize(flog *log.Entry, msg *config.Message, name string, size
|
||||
}
|
||||
}
|
||||
}
|
||||
flog.Debugf("Trying to download %#v with size %#v", name, size)
|
||||
logger.Debugf("Trying to download %#v with size %#v", name, size)
|
||||
if int(size) > general.MediaDownloadSize {
|
||||
msg.Event = config.EventFileFailureSize
|
||||
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{Name: name, Comment: msg.Text, Size: size})
|
||||
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{
|
||||
Name: name,
|
||||
Comment: msg.Text,
|
||||
Size: size,
|
||||
})
|
||||
return fmt.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, general.MediaDownloadSize)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleDownloadData(flog *log.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
|
||||
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
|
||||
func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
|
||||
var avatar bool
|
||||
flog.Debugf("Download OK %#v %#v", name, len(*data))
|
||||
logger.Debugf("Download OK %#v %#v", name, len(*data))
|
||||
if msg.Event == config.EventAvatarDownload {
|
||||
avatar = true
|
||||
}
|
||||
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, URL: url, Comment: comment, Avatar: avatar})
|
||||
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{
|
||||
Name: name,
|
||||
Data: data,
|
||||
URL: url,
|
||||
Comment: comment,
|
||||
Avatar: avatar,
|
||||
})
|
||||
}
|
||||
|
||||
var emptyLineMatcher = regexp.MustCompile("\n+")
|
||||
|
||||
// RemoveEmptyNewLines collapses consecutive newline characters into a single one and
|
||||
// trims any preceding or trailing newline characters as well.
|
||||
func RemoveEmptyNewLines(msg string) string {
|
||||
lines := ""
|
||||
for _, line := range strings.Split(msg, "\n") {
|
||||
if line != "" {
|
||||
lines += line + "\n"
|
||||
}
|
||||
}
|
||||
lines = strings.TrimRight(lines, "\n")
|
||||
return lines
|
||||
return emptyLineMatcher.ReplaceAllString(strings.Trim(msg, "\n"), "\n")
|
||||
}
|
||||
|
||||
// ClipMessage trims a message to the specified length if it exceeds it and adds a warning
|
||||
// to the message in case it does so.
|
||||
func ClipMessage(text string, length int) string {
|
||||
// clip too long messages
|
||||
const clippingMessage = " <clipped message>"
|
||||
if len(text) > length {
|
||||
text = text[:length-len(" *message clipped*")]
|
||||
text = text[:length-len(clippingMessage)]
|
||||
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
|
||||
text = text[:len(text)-size]
|
||||
}
|
||||
text += " *message clipped*"
|
||||
text += clippingMessage
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func ParseMarkdown(input string) string {
|
||||
md := markdown.New(markdown.XHTMLOutput(true), markdown.Breaks(true))
|
||||
res := md.RenderToString([]byte(input))
|
||||
res = strings.TrimPrefix(res, "<p>")
|
||||
res = strings.TrimSuffix(res, "</p>\n")
|
||||
return res
|
||||
}
|
||||
|
||||
// ConvertWebPToPNG convert input data (which should be WebP format to PNG format)
|
||||
func ConvertWebPToPNG(data *[]byte) error {
|
||||
r := bytes.NewReader(*data)
|
||||
m, err := webp.Decode(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var output []byte
|
||||
w := bytes.NewBuffer(output)
|
||||
if err := png.Encode(w, m); err != nil {
|
||||
return err
|
||||
}
|
||||
*data = w.Bytes()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -103,3 +105,22 @@ func TestGetSubLines(t *testing.T) {
|
||||
assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertWebPToPNG(t *testing.T) {
|
||||
if os.Getenv("LOCAL_TEST") == "" {
|
||||
t.Skip()
|
||||
}
|
||||
input, err := ioutil.ReadFile("test.webp")
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
d := &input
|
||||
err = ConvertWebPToPNG(d)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
err = ioutil.WriteFile("test.png", *d, 0644)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
238
bridge/irc/handlers.go
Normal file
238
bridge/irc/handlers.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package birc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/dfordsoft/golib/ic"
|
||||
"github.com/lrstanley/girc"
|
||||
"github.com/paulrosania/go-charset/charset"
|
||||
"github.com/saintfish/chardet"
|
||||
|
||||
// We need to import the 'data' package as an implicit dependency.
|
||||
// See: https://godoc.org/github.com/paulrosania/go-charset/charset
|
||||
_ "github.com/paulrosania/go-charset/data"
|
||||
)
|
||||
|
||||
func (b *Birc) handleCharset(msg *config.Message) error {
|
||||
if b.GetString("Charset") != "" {
|
||||
switch b.GetString("Charset") {
|
||||
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
|
||||
msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text)
|
||||
default:
|
||||
buf := new(bytes.Buffer)
|
||||
w, err := charset.NewWriter(b.GetString("Charset"), buf)
|
||||
if err != nil {
|
||||
b.Log.Errorf("charset from utf-8 conversion failed: %s", err)
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(w, msg.Text)
|
||||
w.Close()
|
||||
msg.Text = buf.String()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleFiles returns true if we have handled the files, otherwise return false
|
||||
func (b *Birc) handleFiles(msg *config.Message) bool {
|
||||
if msg.Extra == nil {
|
||||
return false
|
||||
}
|
||||
for _, rmsg := range helper.HandleExtra(msg, b.General) {
|
||||
b.Local <- rmsg
|
||||
}
|
||||
if len(msg.Extra["file"]) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text = fi.URL
|
||||
if fi.Comment != "" {
|
||||
msg.Text = fi.Comment + ": " + fi.URL
|
||||
}
|
||||
}
|
||||
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
|
||||
if len(event.Params) == 0 {
|
||||
b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
|
||||
return
|
||||
}
|
||||
channel := strings.ToLower(event.Params[0])
|
||||
if event.Command == "KICK" && event.Params[1] == b.Nick {
|
||||
b.Log.Infof("Got kicked from %s by %s", channel, event.Source.Name)
|
||||
time.Sleep(time.Duration(b.GetInt("RejoinDelay")) * time.Second)
|
||||
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EventRejoinChannels}
|
||||
return
|
||||
}
|
||||
if event.Command == "QUIT" {
|
||||
if event.Source.Name == b.Nick && strings.Contains(event.Last(), "Ping timeout") {
|
||||
b.Log.Infof("%s reconnecting ..", b.Account)
|
||||
b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EventFailure}
|
||||
return
|
||||
}
|
||||
}
|
||||
if event.Source.Name != b.Nick {
|
||||
if b.GetBool("nosendjoinpart") {
|
||||
return
|
||||
}
|
||||
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.Remote <- msg
|
||||
return
|
||||
}
|
||||
b.Log.Debugf("handle %#v", event)
|
||||
}
|
||||
|
||||
func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
|
||||
b.Log.Debug("Registering callbacks")
|
||||
i := b.i
|
||||
b.Nick = event.Params[0]
|
||||
|
||||
i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
|
||||
i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg)
|
||||
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
|
||||
i.Handlers.Add(girc.NOTICE, b.handleNotice)
|
||||
i.Handlers.Add("JOIN", b.handleJoinPart)
|
||||
i.Handlers.Add("PART", b.handleJoinPart)
|
||||
i.Handlers.Add("QUIT", b.handleJoinPart)
|
||||
i.Handlers.Add("KICK", b.handleJoinPart)
|
||||
}
|
||||
|
||||
func (b *Birc) handleNickServ() {
|
||||
if !b.GetBool("UseSASL") && b.GetString("NickServNick") != "" && b.GetString("NickServPassword") != "" {
|
||||
b.Log.Debugf("Sending identify to nickserv %s", b.GetString("NickServNick"))
|
||||
b.i.Cmd.Message(b.GetString("NickServNick"), "IDENTIFY "+b.GetString("NickServPassword"))
|
||||
}
|
||||
if strings.EqualFold(b.GetString("NickServNick"), "Q@CServe.quakenet.org") {
|
||||
b.Log.Debugf("Authenticating %s against %s", b.GetString("NickServUsername"), b.GetString("NickServNick"))
|
||||
b.i.Cmd.Message(b.GetString("NickServNick"), "AUTH "+b.GetString("NickServUsername")+" "+b.GetString("NickServPassword"))
|
||||
}
|
||||
// give nickserv some slack
|
||||
time.Sleep(time.Second * 5)
|
||||
b.authDone = true
|
||||
}
|
||||
|
||||
func (b *Birc) handleNotice(client *girc.Client, event girc.Event) {
|
||||
if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.GetString("NickServNick") {
|
||||
b.handleNickServ()
|
||||
} else {
|
||||
b.handlePrivMsg(client, event)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Birc) handleOther(client *girc.Client, event girc.Event) {
|
||||
if b.GetInt("DebugLevel") == 1 {
|
||||
if event.Command != "CLIENT_STATE_UPDATED" &&
|
||||
event.Command != "CLIENT_GENERAL_UPDATED" {
|
||||
b.Log.Debugf("%#v", event.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
switch event.Command {
|
||||
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
|
||||
return
|
||||
}
|
||||
b.Log.Debugf("%#v", event.String())
|
||||
}
|
||||
|
||||
func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
|
||||
b.handleNickServ()
|
||||
b.handleRunCommands()
|
||||
// we are now fully connected
|
||||
// only send on first connection
|
||||
if b.FirstConnection {
|
||||
b.connected <- nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
|
||||
if b.skipPrivMsg(event) {
|
||||
return
|
||||
}
|
||||
rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
|
||||
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
|
||||
|
||||
// set action event
|
||||
if event.IsAction() {
|
||||
rmsg.Event = config.EventUserAction
|
||||
}
|
||||
|
||||
// strip action, we made an event if it was an action
|
||||
rmsg.Text += event.StripAction()
|
||||
|
||||
// start detecting the charset
|
||||
mycharset := b.GetString("Charset")
|
||||
if mycharset == "" {
|
||||
// detect what were sending so that we convert it to utf-8
|
||||
detector := chardet.NewTextDetector()
|
||||
result, err := detector.DetectBest([]byte(rmsg.Text))
|
||||
if err != nil {
|
||||
b.Log.Infof("detection failed for rmsg.Text: %#v", rmsg.Text)
|
||||
return
|
||||
}
|
||||
b.Log.Debugf("detected %s confidence %#v", result.Charset, result.Confidence)
|
||||
mycharset = result.Charset
|
||||
// if we're not sure, just pick ISO-8859-1
|
||||
if result.Confidence < 80 {
|
||||
mycharset = "ISO-8859-1"
|
||||
}
|
||||
}
|
||||
switch mycharset {
|
||||
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
|
||||
rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text)
|
||||
default:
|
||||
r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
|
||||
if err != nil {
|
||||
b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
|
||||
return
|
||||
}
|
||||
output, _ := ioutil.ReadAll(r)
|
||||
rmsg.Text = string(output)
|
||||
}
|
||||
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
func (b *Birc) handleRunCommands() {
|
||||
for _, cmd := range b.GetStringSlice("RunCommands") {
|
||||
if err := b.i.Cmd.SendRaw(cmd); err != nil {
|
||||
b.Log.Errorf("RunCommands %s failed: %s", cmd, err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) {
|
||||
parts := strings.Split(event.Params[2], "!")
|
||||
t, err := strconv.ParseInt(event.Params[3], 10, 64)
|
||||
if err != nil {
|
||||
b.Log.Errorf("Invalid time stamp: %s", event.Params[3])
|
||||
}
|
||||
user := parts[0]
|
||||
if len(parts) > 1 {
|
||||
user += " [" + parts[1] + "]"
|
||||
}
|
||||
b.Log.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0))
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
package birc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -17,10 +13,7 @@ import (
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/dfordsoft/golib/ic"
|
||||
"github.com/lrstanley/girc"
|
||||
"github.com/paulrosania/go-charset/charset"
|
||||
"github.com/saintfish/chardet"
|
||||
|
||||
// We need to import the 'data' package as an implicit dependency.
|
||||
// See: https://godoc.org/github.com/paulrosania/go-charset/charset
|
||||
@@ -68,7 +61,7 @@ func (b *Birc) Command(msg *config.Message) string {
|
||||
if msg.Text == "!users" {
|
||||
b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames)
|
||||
b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames)
|
||||
b.i.Cmd.SendRaw("NAMES " + msg.Channel)
|
||||
b.i.Cmd.SendRaw("NAMES " + msg.Channel) //nolint:errcheck
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -76,35 +69,11 @@ func (b *Birc) Command(msg *config.Message) string {
|
||||
func (b *Birc) Connect() error {
|
||||
b.Local = make(chan config.Message, b.MessageQueue+10)
|
||||
b.Log.Infof("Connecting %s", b.GetString("Server"))
|
||||
server, portstr, err := net.SplitHostPort(b.GetString("Server"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port, err := strconv.Atoi(portstr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// fix strict user handling of girc
|
||||
user := b.GetString("Nick")
|
||||
for !girc.IsValidUser(user) {
|
||||
if len(user) == 1 {
|
||||
user = "matterbridge"
|
||||
break
|
||||
}
|
||||
user = user[1:]
|
||||
}
|
||||
|
||||
i := girc.New(girc.Config{
|
||||
Server: server,
|
||||
ServerPass: b.GetString("Password"),
|
||||
Port: port,
|
||||
Nick: b.GetString("Nick"),
|
||||
User: user,
|
||||
Name: b.GetString("Nick"),
|
||||
SSL: b.GetBool("UseTLS"),
|
||||
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server},
|
||||
PingDelay: time.Minute,
|
||||
})
|
||||
i, err := b.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.GetBool("UseSASL") {
|
||||
i.Config.SASL = &girc.SASLPlain{
|
||||
@@ -115,30 +84,12 @@ func (b *Birc) Connect() error {
|
||||
|
||||
i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection)
|
||||
i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
|
||||
i.Handlers.Add(girc.ERR_NOMOTD, b.handleOtherAuth)
|
||||
i.Handlers.Add(girc.ALL_EVENTS, b.handleOther)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if err := i.Connect(); err != nil {
|
||||
b.Log.Errorf("disconnect: error: %s", err)
|
||||
if b.FirstConnection {
|
||||
b.connected <- err
|
||||
return
|
||||
}
|
||||
} else {
|
||||
b.Log.Info("disconnect: client requested quit")
|
||||
}
|
||||
b.Log.Info("reconnecting in 30 seconds...")
|
||||
time.Sleep(30 * time.Second)
|
||||
i.Handlers.Clear(girc.RPL_WELCOME)
|
||||
i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) {
|
||||
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels}
|
||||
// set our correct nick on reconnect if necessary
|
||||
b.Nick = event.Source.Name
|
||||
})
|
||||
}
|
||||
}()
|
||||
b.i = i
|
||||
|
||||
go b.doConnect()
|
||||
|
||||
err = <-b.connected
|
||||
if err != nil {
|
||||
return fmt.Errorf("connection failed %s", err)
|
||||
@@ -172,7 +123,6 @@ func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
|
||||
} else {
|
||||
b.i.Cmd.Join(channel.Name)
|
||||
}
|
||||
b.authDone = false
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -187,6 +137,7 @@ func (b *Birc) Send(msg config.Message) (string, error) {
|
||||
// we can be in between reconnects #385
|
||||
if !b.i.IsConnected() {
|
||||
b.Log.Error("Not connected to server, dropping message")
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Execute a command
|
||||
@@ -195,44 +146,13 @@ func (b *Birc) Send(msg config.Message) (string, error) {
|
||||
}
|
||||
|
||||
// convert to specified charset
|
||||
if b.GetString("Charset") != "" {
|
||||
switch b.GetString("Charset") {
|
||||
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
|
||||
msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text)
|
||||
default:
|
||||
buf := new(bytes.Buffer)
|
||||
w, err := charset.NewWriter(b.GetString("Charset"), buf)
|
||||
if err != nil {
|
||||
b.Log.Errorf("charset from utf-8 conversion failed: %s", err)
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprint(w, msg.Text)
|
||||
w.Close()
|
||||
msg.Text = buf.String()
|
||||
}
|
||||
if err := b.handleCharset(&msg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Handle files
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.Local <- rmsg
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text = fi.URL
|
||||
if fi.Comment != "" {
|
||||
msg.Text = fi.Comment + ": " + fi.URL
|
||||
}
|
||||
}
|
||||
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
// handle files, return if we're done here
|
||||
if ok := b.handleFiles(&msg); ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var msgLines []string
|
||||
@@ -257,6 +177,28 @@ func (b *Birc) Send(msg config.Message) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (b *Birc) doConnect() {
|
||||
for {
|
||||
if err := b.i.Connect(); err != nil {
|
||||
b.Log.Errorf("disconnect: error: %s", err)
|
||||
if b.FirstConnection {
|
||||
b.connected <- err
|
||||
return
|
||||
}
|
||||
} else {
|
||||
b.Log.Info("disconnect: client requested quit")
|
||||
}
|
||||
b.Log.Info("reconnecting in 30 seconds...")
|
||||
time.Sleep(30 * time.Second)
|
||||
b.i.Handlers.Clear(girc.RPL_WELCOME)
|
||||
b.i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) {
|
||||
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels}
|
||||
// set our correct nick on reconnect if necessary
|
||||
b.Nick = event.Source.Name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Birc) doSend() {
|
||||
rate := time.Millisecond * time.Duration(b.MessageDelay)
|
||||
throttle := time.NewTicker(rate)
|
||||
@@ -277,6 +219,40 @@ func (b *Birc) doSend() {
|
||||
}
|
||||
}
|
||||
|
||||
// validateInput validates the server/port/nick configuration. Returns a *girc.Client if successful
|
||||
func (b *Birc) getClient() (*girc.Client, error) {
|
||||
server, portstr, err := net.SplitHostPort(b.GetString("Server"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port, err := strconv.Atoi(portstr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// fix strict user handling of girc
|
||||
user := b.GetString("Nick")
|
||||
for !girc.IsValidUser(user) {
|
||||
if len(user) == 1 || len(user) == 0 {
|
||||
user = "matterbridge"
|
||||
break
|
||||
}
|
||||
user = user[1:]
|
||||
}
|
||||
|
||||
i := girc.New(girc.Config{
|
||||
Server: server,
|
||||
ServerPass: b.GetString("Password"),
|
||||
Port: port,
|
||||
Nick: b.GetString("Nick"),
|
||||
User: user,
|
||||
Name: b.GetString("Nick"),
|
||||
SSL: b.GetBool("UseTLS"),
|
||||
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec
|
||||
PingDelay: time.Minute,
|
||||
})
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (b *Birc) endNames(client *girc.Client, event girc.Event) {
|
||||
channel := event.Params[1]
|
||||
sort.Strings(b.names[channel])
|
||||
@@ -293,82 +269,6 @@ func (b *Birc) endNames(client *girc.Client, event girc.Event) {
|
||||
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
|
||||
}
|
||||
|
||||
func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
|
||||
b.Log.Debug("Registering callbacks")
|
||||
i := b.i
|
||||
b.Nick = event.Params[0]
|
||||
|
||||
i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
|
||||
i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg)
|
||||
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
|
||||
i.Handlers.Add(girc.NOTICE, b.handleNotice)
|
||||
i.Handlers.Add("JOIN", b.handleJoinPart)
|
||||
i.Handlers.Add("PART", b.handleJoinPart)
|
||||
i.Handlers.Add("QUIT", b.handleJoinPart)
|
||||
i.Handlers.Add("KICK", b.handleJoinPart)
|
||||
}
|
||||
|
||||
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
|
||||
if len(event.Params) == 0 {
|
||||
b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
|
||||
return
|
||||
}
|
||||
channel := strings.ToLower(event.Params[0])
|
||||
if event.Command == "KICK" && event.Params[1] == b.Nick {
|
||||
b.Log.Infof("Got kicked from %s by %s", channel, event.Source.Name)
|
||||
time.Sleep(time.Duration(b.GetInt("RejoinDelay")) * time.Second)
|
||||
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EventRejoinChannels}
|
||||
return
|
||||
}
|
||||
if event.Command == "QUIT" {
|
||||
if event.Source.Name == b.Nick && strings.Contains(event.Trailing, "Ping timeout") {
|
||||
b.Log.Infof("%s reconnecting ..", b.Account)
|
||||
b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EventFailure}
|
||||
return
|
||||
}
|
||||
}
|
||||
if event.Source.Name != b.Nick {
|
||||
if b.GetBool("nosendjoinpart") {
|
||||
return
|
||||
}
|
||||
b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account)
|
||||
msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
|
||||
b.Log.Debugf("<= Message is %#v", msg)
|
||||
b.Remote <- msg
|
||||
return
|
||||
}
|
||||
b.Log.Debugf("handle %#v", event)
|
||||
}
|
||||
|
||||
func (b *Birc) handleNotice(client *girc.Client, event girc.Event) {
|
||||
if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.GetString("NickServNick") {
|
||||
b.handleNickServ()
|
||||
} else {
|
||||
b.handlePrivMsg(client, event)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Birc) handleOther(client *girc.Client, event girc.Event) {
|
||||
if b.GetInt("DebugLevel") == 1 {
|
||||
if event.Command != "CLIENT_STATE_UPDATED" &&
|
||||
event.Command != "CLIENT_GENERAL_UPDATED" {
|
||||
b.Log.Debugf("%#v", event.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
switch event.Command {
|
||||
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
|
||||
return
|
||||
}
|
||||
b.Log.Debugf("%#v", event.String())
|
||||
}
|
||||
|
||||
func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
|
||||
b.handleNickServ()
|
||||
// we are now fully connected
|
||||
b.connected <- nil
|
||||
}
|
||||
|
||||
func (b *Birc) skipPrivMsg(event girc.Event) bool {
|
||||
// Our nick can be changed
|
||||
b.Nick = b.i.GetNick()
|
||||
@@ -388,74 +288,6 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
|
||||
if b.skipPrivMsg(event) {
|
||||
return
|
||||
}
|
||||
rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
|
||||
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Trailing, event)
|
||||
|
||||
// set action event
|
||||
if event.IsAction() {
|
||||
rmsg.Event = config.EventUserAction
|
||||
}
|
||||
|
||||
// strip action, we made an event if it was an action
|
||||
rmsg.Text += event.StripAction()
|
||||
|
||||
// strip IRC colors
|
||||
re := regexp.MustCompile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
|
||||
rmsg.Text = re.ReplaceAllString(rmsg.Text, "")
|
||||
|
||||
// start detecting the charset
|
||||
var r io.Reader
|
||||
var err error
|
||||
mycharset := b.GetString("Charset")
|
||||
if mycharset == "" {
|
||||
// detect what were sending so that we convert it to utf-8
|
||||
detector := chardet.NewTextDetector()
|
||||
result, err := detector.DetectBest([]byte(rmsg.Text))
|
||||
if err != nil {
|
||||
b.Log.Infof("detection failed for rmsg.Text: %#v", rmsg.Text)
|
||||
return
|
||||
}
|
||||
b.Log.Debugf("detected %s confidence %#v", result.Charset, result.Confidence)
|
||||
mycharset = result.Charset
|
||||
// if we're not sure, just pick ISO-8859-1
|
||||
if result.Confidence < 80 {
|
||||
mycharset = "ISO-8859-1"
|
||||
}
|
||||
}
|
||||
switch mycharset {
|
||||
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
|
||||
rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text)
|
||||
default:
|
||||
r, err = charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
|
||||
if err != nil {
|
||||
b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
|
||||
return
|
||||
}
|
||||
output, _ := ioutil.ReadAll(r)
|
||||
rmsg.Text = string(output)
|
||||
}
|
||||
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) {
|
||||
parts := strings.Split(event.Params[2], "!")
|
||||
t, err := strconv.ParseInt(event.Params[3], 10, 64)
|
||||
if err != nil {
|
||||
b.Log.Errorf("Invalid time stamp: %s", event.Params[3])
|
||||
}
|
||||
user := parts[0]
|
||||
if len(parts) > 1 {
|
||||
user += " [" + parts[1] + "]"
|
||||
}
|
||||
b.Log.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0))
|
||||
}
|
||||
|
||||
func (b *Birc) nicksPerRow() int {
|
||||
return 4
|
||||
}
|
||||
@@ -464,23 +296,9 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
|
||||
channel := event.Params[2]
|
||||
b.names[channel] = append(
|
||||
b.names[channel],
|
||||
strings.Split(strings.TrimSpace(event.Trailing), " ")...)
|
||||
strings.Split(strings.TrimSpace(event.Last()), " ")...)
|
||||
}
|
||||
|
||||
func (b *Birc) formatnicks(nicks []string) string {
|
||||
return strings.Join(nicks, ", ") + " currently on IRC"
|
||||
}
|
||||
|
||||
func (b *Birc) handleNickServ() {
|
||||
if !b.GetBool("UseSASL") && b.GetString("NickServNick") != "" && b.GetString("NickServPassword") != "" {
|
||||
b.Log.Debugf("Sending identify to nickserv %s", b.GetString("NickServNick"))
|
||||
b.i.Cmd.Message(b.GetString("NickServNick"), "IDENTIFY "+b.GetString("NickServPassword"))
|
||||
}
|
||||
if strings.EqualFold(b.GetString("NickServNick"), "Q@CServe.quakenet.org") {
|
||||
b.Log.Debugf("Authenticating %s against %s", b.GetString("NickServUsername"), b.GetString("NickServNick"))
|
||||
b.i.Cmd.Message(b.GetString("NickServNick"), "AUTH "+b.GetString("NickServUsername")+" "+b.GetString("NickServPassword"))
|
||||
}
|
||||
// give nickserv some slack
|
||||
time.Sleep(time.Second * 5)
|
||||
b.authDone = true
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package bmatrix
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"mime"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -19,11 +20,13 @@ type Bmatrix struct {
|
||||
UserID string
|
||||
RoomMap map[string]string
|
||||
sync.RWMutex
|
||||
htmlTag *regexp.Regexp
|
||||
*bridge.Config
|
||||
}
|
||||
|
||||
func New(cfg *bridge.Config) bridge.Bridger {
|
||||
b := &Bmatrix{Config: cfg}
|
||||
b.htmlTag = regexp.MustCompile("</.*?>")
|
||||
b.RoomMap = make(map[string]string)
|
||||
return b
|
||||
}
|
||||
@@ -99,19 +102,35 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
|
||||
// Upload a file if it exists
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.mc.SendText(channel, rmsg.Username+rmsg.Text)
|
||||
if _, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text); err != nil {
|
||||
b.Log.Errorf("sendText failed: %s", err)
|
||||
}
|
||||
}
|
||||
// check if we have files to upload (from slack, telegram or mattermost)
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
return b.handleUploadFile(&msg, channel)
|
||||
return b.handleUploadFiles(&msg, channel)
|
||||
}
|
||||
}
|
||||
|
||||
// Edit message if we have an ID
|
||||
// matrix has no editing support
|
||||
|
||||
// Post normal message
|
||||
resp, err := b.mc.SendText(channel, msg.Username+msg.Text)
|
||||
// Use notices to send join/leave events
|
||||
if msg.Event == config.EventJoinLeave {
|
||||
resp, err := b.mc.SendNotice(channel, msg.Username+msg.Text)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.EventID, err
|
||||
}
|
||||
|
||||
username := html.EscapeString(msg.Username)
|
||||
// check if we have a </tag>. if we have, we don't escape HTML. #696
|
||||
if b.htmlTag.MatchString(msg.Username) {
|
||||
username = msg.Username
|
||||
}
|
||||
// Post normal message with HTML support (eg riot.im)
|
||||
resp, err := b.mc.SendHTML(channel, msg.Username+msg.Text, username+helper.ParseMarkdown(msg.Text))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -257,47 +276,60 @@ func (b *Bmatrix) handleDownloadFile(rmsg *config.Message, content map[string]in
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleUploadFile handles native upload of files
|
||||
func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string) (string, error) {
|
||||
// handleUploadFiles handles native upload of files.
|
||||
func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string, error) {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
content := bytes.NewReader(*fi.Data)
|
||||
sp := strings.Split(fi.Name, ".")
|
||||
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
|
||||
if strings.Contains(mtype, "image") ||
|
||||
strings.Contains(mtype, "video") {
|
||||
if fi.Comment != "" {
|
||||
_, err := b.mc.SendText(channel, msg.Username+fi.Comment)
|
||||
if err != nil {
|
||||
b.Log.Errorf("file comment failed: %#v", err)
|
||||
}
|
||||
}
|
||||
b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
|
||||
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
|
||||
if err != nil {
|
||||
b.Log.Errorf("file upload failed: %#v", err)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(mtype, "video") {
|
||||
b.Log.Debugf("sendVideo %s", res.ContentURI)
|
||||
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
|
||||
if err != nil {
|
||||
b.Log.Errorf("sendVideo failed: %#v", err)
|
||||
}
|
||||
}
|
||||
if strings.Contains(mtype, "image") {
|
||||
b.Log.Debugf("sendImage %s", res.ContentURI)
|
||||
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
|
||||
if err != nil {
|
||||
b.Log.Errorf("sendImage failed: %#v", err)
|
||||
}
|
||||
}
|
||||
b.Log.Debugf("result: %#v", res)
|
||||
if fi, ok := f.(config.FileInfo); ok {
|
||||
b.handleUploadFile(msg, channel, &fi)
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// handleUploadFile handles native upload of a file.
|
||||
func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) {
|
||||
content := bytes.NewReader(*fi.Data)
|
||||
sp := strings.Split(fi.Name, ".")
|
||||
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
|
||||
if !strings.Contains(mtype, "image") && !strings.Contains(mtype, "video") {
|
||||
return
|
||||
}
|
||||
if fi.Comment != "" {
|
||||
_, err := b.mc.SendText(channel, msg.Username+fi.Comment)
|
||||
if err != nil {
|
||||
b.Log.Errorf("file comment failed: %#v", err)
|
||||
}
|
||||
} else {
|
||||
// image and video uploads send no username, we have to do this ourself here #715
|
||||
_, err := b.mc.SendText(channel, msg.Username)
|
||||
if err != nil {
|
||||
b.Log.Errorf("file comment failed: %#v", err)
|
||||
}
|
||||
}
|
||||
b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
|
||||
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
|
||||
if err != nil {
|
||||
b.Log.Errorf("file upload failed: %#v", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(mtype, "video"):
|
||||
b.Log.Debugf("sendVideo %s", res.ContentURI)
|
||||
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
|
||||
if err != nil {
|
||||
b.Log.Errorf("sendVideo failed: %#v", err)
|
||||
}
|
||||
case strings.Contains(mtype, "image"):
|
||||
b.Log.Debugf("sendImage %s", res.ContentURI)
|
||||
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
|
||||
if err != nil {
|
||||
b.Log.Errorf("sendImage failed: %#v", err)
|
||||
}
|
||||
}
|
||||
b.Log.Debugf("result: %#v", res)
|
||||
}
|
||||
|
||||
// skipMessages returns true if this message should not be handled
|
||||
func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool {
|
||||
// Skip empty messages
|
||||
|
||||
195
bridge/mattermost/handlers.go
Normal file
195
bridge/mattermost/handlers.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package bmattermost
|
||||
|
||||
import (
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/42wim/matterbridge/matterclient"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
// handleDownloadAvatar downloads the avatar of userid from channel
|
||||
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
|
||||
// logs an error message if it fails
|
||||
func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
|
||||
rmsg := config.Message{
|
||||
Username: "system",
|
||||
Text: "avatar",
|
||||
Channel: channel,
|
||||
Account: b.Account,
|
||||
UserID: userid,
|
||||
Event: config.EventAvatarDownload,
|
||||
Extra: make(map[string][]interface{}),
|
||||
}
|
||||
if _, ok := b.avatarMap[userid]; !ok {
|
||||
data, resp := b.mc.Client.GetProfileImage(userid, "")
|
||||
if resp.Error != nil {
|
||||
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
|
||||
return
|
||||
}
|
||||
err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
|
||||
if err != nil {
|
||||
b.Log.Error(err)
|
||||
return
|
||||
}
|
||||
helper.HandleDownloadData(b.Log, &rmsg, userid+".png", rmsg.Text, "", &data, b.General)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
}
|
||||
|
||||
// handleDownloadFile handles file download
|
||||
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
|
||||
url, _ := b.mc.Client.GetFileLink(id)
|
||||
finfo, resp := b.mc.Client.GetFileInfo(id)
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, resp := b.mc.Client.DownloadFile(id, true)
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bmattermost) handleMatter() {
|
||||
messages := make(chan *config.Message)
|
||||
if b.GetString("WebhookBindAddress") != "" {
|
||||
b.Log.Debugf("Choosing webhooks based receiving")
|
||||
go b.handleMatterHook(messages)
|
||||
} else {
|
||||
if b.GetString("Token") != "" {
|
||||
b.Log.Debugf("Choosing token based receiving")
|
||||
} else {
|
||||
b.Log.Debugf("Choosing login/password based receiving")
|
||||
}
|
||||
go b.handleMatterClient(messages)
|
||||
}
|
||||
var ok bool
|
||||
for message := range messages {
|
||||
message.Avatar = helper.GetAvatar(b.avatarMap, message.UserID, b.General)
|
||||
message.Account = b.Account
|
||||
message.Text, ok = b.replaceAction(message.Text)
|
||||
if ok {
|
||||
message.Event = config.EventUserAction
|
||||
}
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", message)
|
||||
b.Remote <- *message
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
|
||||
for message := range b.mc.MessageChan {
|
||||
b.Log.Debugf("%#v", message.Raw.Data)
|
||||
|
||||
if b.skipMessage(message) {
|
||||
b.Log.Debugf("Skipped message: %#v", message)
|
||||
continue
|
||||
}
|
||||
|
||||
// only download avatars if we have a place to upload them (configured mediaserver)
|
||||
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
|
||||
b.handleDownloadAvatar(message.UserID, message.Channel)
|
||||
}
|
||||
|
||||
b.Log.Debugf("== Receiving event %#v", message)
|
||||
|
||||
rmsg := &config.Message{
|
||||
Username: message.Username,
|
||||
UserID: message.UserID,
|
||||
Channel: message.Channel,
|
||||
Text: message.Text,
|
||||
ID: message.Post.Id,
|
||||
ParentID: message.Post.ParentId,
|
||||
Extra: make(map[string][]interface{}),
|
||||
}
|
||||
|
||||
// handle mattermost post properties (override username and attachments)
|
||||
b.handleProps(rmsg, message)
|
||||
|
||||
// create a text for bridges that don't support native editing
|
||||
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") {
|
||||
rmsg.Text = message.Text + b.GetString("EditSuffix")
|
||||
}
|
||||
|
||||
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED {
|
||||
rmsg.Event = config.EventMsgDelete
|
||||
}
|
||||
|
||||
for _, id := range message.Post.FileIds {
|
||||
err := b.handleDownloadFile(rmsg, id)
|
||||
if err != nil {
|
||||
b.Log.Errorf("download failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Use nickname instead of username if defined
|
||||
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
|
||||
rmsg.Username = nick
|
||||
}
|
||||
|
||||
messages <- rmsg
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
|
||||
for {
|
||||
message := b.mh.Receive()
|
||||
b.Log.Debugf("Receiving from matterhook %#v", message)
|
||||
messages <- &config.Message{
|
||||
UserID: message.UserID,
|
||||
Username: message.UserName,
|
||||
Text: message.Text,
|
||||
Channel: message.ChannelName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleUploadFile handles native upload of files
|
||||
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
|
||||
var err error
|
||||
var res, id string
|
||||
channelID := b.mc.GetChannelId(msg.Channel, b.TeamID)
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
msg.Text = fi.Comment
|
||||
if b.GetBool("PrefixMessagesWithNick") {
|
||||
msg.Text = msg.Username + msg.Text
|
||||
}
|
||||
res, err = b.mc.PostMessageWithFiles(channelID, msg.Text, msg.ParentID, []string{id})
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
|
||||
props := message.Post.Props
|
||||
if props == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := props["override_username"].(string); ok {
|
||||
rmsg.Username = props["override_username"].(string)
|
||||
}
|
||||
if _, ok := props["attachments"].([]interface{}); ok {
|
||||
rmsg.Extra["attachments"] = props["attachments"].([]interface{})
|
||||
if rmsg.Text == "" {
|
||||
for _, attachment := range rmsg.Extra["attachments"] {
|
||||
attach := attachment.(map[string]interface{})
|
||||
if attach["text"].(string) != "" {
|
||||
rmsg.Text += attach["text"].(string)
|
||||
continue
|
||||
}
|
||||
if attach["fallback"].(string) != "" {
|
||||
rmsg.Text += attach["fallback"].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
225
bridge/mattermost/helpers.go
Normal file
225
bridge/mattermost/helpers.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package bmattermost
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/42wim/matterbridge/matterclient"
|
||||
"github.com/42wim/matterbridge/matterhook"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
func (b *Bmattermost) doConnectWebhookBind() error {
|
||||
switch {
|
||||
case b.GetString("WebhookURL") != "":
|
||||
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
|
||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||
BindAddress: b.GetString("WebhookBindAddress")})
|
||||
case b.GetString("Token") != "":
|
||||
b.Log.Info("Connecting using token (sending)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case b.GetString("Login") != "":
|
||||
b.Log.Info("Connecting using login/password (sending)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
b.Log.Info("Connecting using webhookbindaddress (receiving)")
|
||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||
BindAddress: b.GetString("WebhookBindAddress")})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bmattermost) doConnectWebhookURL() error {
|
||||
b.Log.Info("Connecting using webhookurl (sending)")
|
||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||
DisableServer: true})
|
||||
if b.GetString("Token") != "" {
|
||||
b.Log.Info("Connecting using token (receiving)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if b.GetString("Login") != "" {
|
||||
b.Log.Info("Connecting using login/password (receiving)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bmattermost) apiLogin() error {
|
||||
password := b.GetString("Password")
|
||||
if b.GetString("Token") != "" {
|
||||
password = "token=" + b.GetString("Token")
|
||||
}
|
||||
|
||||
b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"))
|
||||
if b.GetBool("debug") {
|
||||
b.mc.SetLogLevel("debug")
|
||||
}
|
||||
b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify")
|
||||
b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck")
|
||||
b.mc.NoTLS = b.GetBool("NoTLS")
|
||||
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
|
||||
err := b.mc.Login()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Log.Info("Connection succeeded")
|
||||
b.TeamID = b.mc.GetTeamId()
|
||||
go b.mc.WsReceiver()
|
||||
go b.mc.StatusLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// replaceAction replace the message with the correct action (/me) code
|
||||
func (b *Bmattermost) replaceAction(text string) (string, bool) {
|
||||
if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
|
||||
return strings.Replace(text, "*", "", -1), true
|
||||
}
|
||||
return text, false
|
||||
}
|
||||
|
||||
func (b *Bmattermost) cacheAvatar(msg *config.Message) (string, error) {
|
||||
fi := msg.Extra["file"][0].(config.FileInfo)
|
||||
/* if we have a sha we have successfully uploaded the file to the media server,
|
||||
so we can now cache the sha */
|
||||
if fi.SHA != "" {
|
||||
b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
|
||||
b.avatarMap[msg.UserID] = fi.SHA
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// sendWebhook uses the configured WebhookURL to send the message
|
||||
func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
|
||||
// skip events
|
||||
if msg.Event != "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if b.GetBool("PrefixMessagesWithNick") {
|
||||
msg.Text = msg.Username + msg.Text
|
||||
}
|
||||
if msg.Extra != nil {
|
||||
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
rmsg := rmsg // scopelint
|
||||
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
|
||||
matterMessage := matterhook.OMessage{
|
||||
IconURL: iconURL,
|
||||
Channel: rmsg.Channel,
|
||||
UserName: rmsg.Username,
|
||||
Text: rmsg.Text,
|
||||
Props: make(map[string]interface{}),
|
||||
}
|
||||
matterMessage.Props["matterbridge_"+b.uuid] = true
|
||||
if err := b.mh.Send(matterMessage); err != nil {
|
||||
b.Log.Errorf("sendWebhook failed: %s ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// webhook doesn't support file uploads, so we add the url manually
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.URL != "" {
|
||||
msg.Text += fi.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iconURL := config.GetIconURL(&msg, b.GetString("iconurl"))
|
||||
matterMessage := matterhook.OMessage{
|
||||
IconURL: iconURL,
|
||||
Channel: msg.Channel,
|
||||
UserName: msg.Username,
|
||||
Text: msg.Text,
|
||||
Props: make(map[string]interface{}),
|
||||
}
|
||||
if msg.Avatar != "" {
|
||||
matterMessage.IconURL = msg.Avatar
|
||||
}
|
||||
matterMessage.Props["matterbridge_"+b.uuid] = true
|
||||
err := b.mh.Send(matterMessage)
|
||||
if err != nil {
|
||||
b.Log.Info(err)
|
||||
return "", err
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// skipMessages returns true if this message should not be handled
|
||||
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
|
||||
// Handle join/leave
|
||||
if message.Type == "system_join_leave" ||
|
||||
message.Type == "system_join_channel" ||
|
||||
message.Type == "system_leave_channel" {
|
||||
if b.GetBool("nosendjoinpart") {
|
||||
return true
|
||||
}
|
||||
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
|
||||
b.Remote <- config.Message{
|
||||
Username: "system",
|
||||
Text: message.Text,
|
||||
Channel: message.Channel,
|
||||
Account: b.Account,
|
||||
Event: config.EventJoinLeave,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle edited messages
|
||||
if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Ignore non-post messages
|
||||
if message.Post == nil {
|
||||
b.Log.Debugf("ignoring nil message.Post: %#v", message)
|
||||
return true
|
||||
}
|
||||
|
||||
// Ignore messages sent from matterbridge
|
||||
if message.Post.Props != nil {
|
||||
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
|
||||
b.Log.Debugf("sent by matterbridge, ignoring")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore messages sent from a user logged in as the bot
|
||||
if b.mc.User.Username == message.Username {
|
||||
return true
|
||||
}
|
||||
|
||||
// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
|
||||
if message.Post.HasReactions {
|
||||
return true
|
||||
}
|
||||
|
||||
// ignore messages from other teams than ours
|
||||
if message.Raw.Data["team_id"].(string) != b.TeamID {
|
||||
return true
|
||||
}
|
||||
|
||||
// only handle posted, edited or deleted events
|
||||
if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED ||
|
||||
message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -3,14 +3,12 @@ package bmattermost
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/42wim/matterbridge/matterclient"
|
||||
"github.com/42wim/matterbridge/matterhook"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
@@ -40,54 +38,18 @@ func (b *Bmattermost) Connect() error {
|
||||
return nil
|
||||
}
|
||||
if b.GetString("WebhookBindAddress") != "" {
|
||||
switch {
|
||||
case b.GetString("WebhookURL") != "":
|
||||
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
|
||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||
BindAddress: b.GetString("WebhookBindAddress")})
|
||||
case b.GetString("Token") != "":
|
||||
b.Log.Info("Connecting using token (sending)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case b.GetString("Login") != "":
|
||||
b.Log.Info("Connecting using login/password (sending)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
b.Log.Info("Connecting using webhookbindaddress (receiving)")
|
||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||
BindAddress: b.GetString("WebhookBindAddress")})
|
||||
if err := b.doConnectWebhookBind(); err != nil {
|
||||
return err
|
||||
}
|
||||
go b.handleMatter()
|
||||
return nil
|
||||
}
|
||||
switch {
|
||||
case b.GetString("WebhookURL") != "":
|
||||
b.Log.Info("Connecting using webhookurl (sending)")
|
||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||
DisableServer: true})
|
||||
if b.GetString("Token") != "" {
|
||||
b.Log.Info("Connecting using token (receiving)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go b.handleMatter()
|
||||
} else if b.GetString("Login") != "" {
|
||||
b.Log.Info("Connecting using login/password (receiving)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go b.handleMatter()
|
||||
if err := b.doConnectWebhookURL(); err != nil {
|
||||
return err
|
||||
}
|
||||
go b.handleMatter()
|
||||
return nil
|
||||
case b.GetString("Token") != "":
|
||||
b.Log.Info("Connecting using token (sending and receiving)")
|
||||
@@ -104,7 +66,8 @@ func (b *Bmattermost) Connect() error {
|
||||
}
|
||||
go b.handleMatter()
|
||||
}
|
||||
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" && b.GetString("Login") == "" && b.GetString("Token") == "" {
|
||||
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" &&
|
||||
b.GetString("Login") == "" && b.GetString("Token") == "" {
|
||||
return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token/Login/Password/Server/Team configured")
|
||||
}
|
||||
return nil
|
||||
@@ -158,10 +121,18 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
|
||||
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
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text)
|
||||
if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
|
||||
b.Log.Errorf("PostMessage failed: %s", err)
|
||||
}
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
return b.handleUploadFile(&msg)
|
||||
@@ -179,304 +150,5 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
|
||||
}
|
||||
|
||||
// Post normal message
|
||||
return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text)
|
||||
}
|
||||
|
||||
func (b *Bmattermost) handleMatter() {
|
||||
messages := make(chan *config.Message)
|
||||
if b.GetString("WebhookBindAddress") != "" {
|
||||
b.Log.Debugf("Choosing webhooks based receiving")
|
||||
go b.handleMatterHook(messages)
|
||||
} else {
|
||||
if b.GetString("Token") != "" {
|
||||
b.Log.Debugf("Choosing token based receiving")
|
||||
} else {
|
||||
b.Log.Debugf("Choosing login/password based receiving")
|
||||
}
|
||||
go b.handleMatterClient(messages)
|
||||
}
|
||||
var ok bool
|
||||
for message := range messages {
|
||||
message.Avatar = helper.GetAvatar(b.avatarMap, message.UserID, b.General)
|
||||
message.Account = b.Account
|
||||
message.Text, ok = b.replaceAction(message.Text)
|
||||
if ok {
|
||||
message.Event = config.EventUserAction
|
||||
}
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", message)
|
||||
b.Remote <- *message
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
|
||||
for message := range b.mc.MessageChan {
|
||||
b.Log.Debugf("%#v", message.Raw.Data)
|
||||
|
||||
if b.skipMessage(message) {
|
||||
b.Log.Debugf("Skipped message: %#v", message)
|
||||
continue
|
||||
}
|
||||
|
||||
// only download avatars if we have a place to upload them (configured mediaserver)
|
||||
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
|
||||
b.handleDownloadAvatar(message.UserID, message.Channel)
|
||||
}
|
||||
|
||||
b.Log.Debugf("== Receiving event %#v", message)
|
||||
|
||||
rmsg := &config.Message{Username: message.Username, UserID: message.UserID, Channel: message.Channel, Text: message.Text, ID: message.Post.Id, Extra: make(map[string][]interface{})}
|
||||
|
||||
// handle mattermost post properties (override username and attachments)
|
||||
props := message.Post.Props
|
||||
if props != nil {
|
||||
if _, ok := props["override_username"].(string); ok {
|
||||
rmsg.Username = props["override_username"].(string)
|
||||
}
|
||||
if _, ok := props["attachments"].([]interface{}); ok {
|
||||
rmsg.Extra["attachments"] = props["attachments"].([]interface{})
|
||||
if rmsg.Text == "" {
|
||||
for _, attachment := range rmsg.Extra["attachments"] {
|
||||
attach := attachment.(map[string]interface{})
|
||||
if attach["text"].(string) != "" {
|
||||
rmsg.Text += attach["text"].(string)
|
||||
continue
|
||||
}
|
||||
if attach["fallback"].(string) != "" {
|
||||
rmsg.Text += attach["fallback"].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create a text for bridges that don't support native editing
|
||||
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") {
|
||||
rmsg.Text = message.Text + b.GetString("EditSuffix")
|
||||
}
|
||||
|
||||
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED {
|
||||
rmsg.Event = config.EventMsgDelete
|
||||
}
|
||||
|
||||
if len(message.Post.FileIds) > 0 {
|
||||
for _, id := range message.Post.FileIds {
|
||||
err := b.handleDownloadFile(rmsg, id)
|
||||
if err != nil {
|
||||
b.Log.Errorf("download failed: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use nickname instead of username if defined
|
||||
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
|
||||
rmsg.Username = nick
|
||||
}
|
||||
|
||||
messages <- rmsg
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
|
||||
for {
|
||||
message := b.mh.Receive()
|
||||
b.Log.Debugf("Receiving from matterhook %#v", message)
|
||||
messages <- &config.Message{UserID: message.UserID, Username: message.UserName, Text: message.Text, Channel: message.ChannelName}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bmattermost) apiLogin() error {
|
||||
password := b.GetString("Password")
|
||||
if b.GetString("Token") != "" {
|
||||
password = "token=" + b.GetString("Token")
|
||||
}
|
||||
|
||||
b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"))
|
||||
if b.GetBool("debug") {
|
||||
b.mc.SetLogLevel("debug")
|
||||
}
|
||||
b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify")
|
||||
b.mc.NoTLS = b.GetBool("NoTLS")
|
||||
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
|
||||
err := b.mc.Login()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Log.Info("Connection succeeded")
|
||||
b.TeamID = b.mc.GetTeamId()
|
||||
go b.mc.WsReceiver()
|
||||
go b.mc.StatusLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// replaceAction replace the message with the correct action (/me) code
|
||||
func (b *Bmattermost) replaceAction(text string) (string, bool) {
|
||||
if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
|
||||
return strings.Replace(text, "*", "", -1), true
|
||||
}
|
||||
return text, false
|
||||
}
|
||||
|
||||
func (b *Bmattermost) cacheAvatar(msg *config.Message) (string, error) {
|
||||
fi := msg.Extra["file"][0].(config.FileInfo)
|
||||
/* if we have a sha we have successfully uploaded the file to the media server,
|
||||
so we can now cache the sha */
|
||||
if fi.SHA != "" {
|
||||
b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
|
||||
b.avatarMap[msg.UserID] = fi.SHA
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// handleDownloadAvatar downloads the avatar of userid from channel
|
||||
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
|
||||
// logs an error message if it fails
|
||||
func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
|
||||
rmsg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: userid, Event: config.EventAvatarDownload, Extra: make(map[string][]interface{})}
|
||||
if _, ok := b.avatarMap[userid]; !ok {
|
||||
data, resp := b.mc.Client.GetProfileImage(userid, "")
|
||||
if resp.Error != nil {
|
||||
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
|
||||
return
|
||||
}
|
||||
err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
|
||||
if err != nil {
|
||||
b.Log.Error(err)
|
||||
return
|
||||
}
|
||||
helper.HandleDownloadData(b.Log, &rmsg, userid+".png", rmsg.Text, "", &data, b.General)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
}
|
||||
|
||||
// handleDownloadFile handles file download
|
||||
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
|
||||
url, _ := b.mc.Client.GetFileLink(id)
|
||||
finfo, resp := b.mc.Client.GetFileInfo(id)
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, resp := b.mc.Client.DownloadFile(id, true)
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleUploadFile handles native upload of files
|
||||
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
|
||||
var err error
|
||||
var res, id string
|
||||
channelID := b.mc.GetChannelId(msg.Channel, b.TeamID)
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
msg.Text = fi.Comment
|
||||
if b.GetBool("PrefixMessagesWithNick") {
|
||||
msg.Text = msg.Username + msg.Text
|
||||
}
|
||||
res, err = b.mc.PostMessageWithFiles(channelID, msg.Text, []string{id})
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// sendWebhook uses the configured WebhookURL to send the message
|
||||
func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
|
||||
// skip events
|
||||
if msg.Event != "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if b.GetBool("PrefixMessagesWithNick") {
|
||||
msg.Text = msg.Username + msg.Text
|
||||
}
|
||||
if msg.Extra != nil {
|
||||
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
rmsg := rmsg // scopelint
|
||||
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
|
||||
matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: rmsg.Channel, UserName: rmsg.Username, Text: rmsg.Text, Props: make(map[string]interface{})}
|
||||
matterMessage.Props["matterbridge_"+b.uuid] = true
|
||||
b.mh.Send(matterMessage)
|
||||
}
|
||||
|
||||
// webhook doesn't support file uploads, so we add the url manually
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.URL != "" {
|
||||
msg.Text += fi.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iconURL := config.GetIconURL(&msg, b.GetString("iconurl"))
|
||||
matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: msg.Channel, UserName: msg.Username, Text: msg.Text, Props: make(map[string]interface{})}
|
||||
if msg.Avatar != "" {
|
||||
matterMessage.IconURL = msg.Avatar
|
||||
}
|
||||
matterMessage.Props["matterbridge_"+b.uuid] = true
|
||||
err := b.mh.Send(matterMessage)
|
||||
if err != nil {
|
||||
b.Log.Info(err)
|
||||
return "", err
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// skipMessages returns true if this message should not be handled
|
||||
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
|
||||
// Handle join/leave
|
||||
if message.Type == "system_join_leave" ||
|
||||
message.Type == "system_join_channel" ||
|
||||
message.Type == "system_leave_channel" {
|
||||
if b.GetBool("nosendjoinpart") {
|
||||
return true
|
||||
}
|
||||
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
|
||||
b.Remote <- config.Message{Username: "system", Text: message.Text, Channel: message.Channel, Account: b.Account, Event: config.EventJoinLeave}
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle edited messages
|
||||
if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Ignore messages sent from matterbridge
|
||||
if message.Post.Props != nil {
|
||||
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
|
||||
b.Log.Debugf("sent by matterbridge, ignoring")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore messages sent from a user logged in as the bot
|
||||
if b.mc.User.Username == message.Username {
|
||||
return true
|
||||
}
|
||||
|
||||
// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
|
||||
if message.Post.HasReactions {
|
||||
return true
|
||||
}
|
||||
|
||||
// ignore messages from other teams than ours
|
||||
if message.Raw.Data["team_id"].(string) != b.TeamID {
|
||||
return true
|
||||
}
|
||||
|
||||
// only handle posted, edited or deleted events
|
||||
if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED || message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID)
|
||||
}
|
||||
|
||||
74
bridge/rocketchat/handlers.go
Normal file
74
bridge/rocketchat/handlers.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package brocketchat
|
||||
|
||||
import (
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
)
|
||||
|
||||
func (b *Brocketchat) handleRocket() {
|
||||
messages := make(chan *config.Message)
|
||||
if b.GetString("WebhookBindAddress") != "" {
|
||||
b.Log.Debugf("Choosing webhooks based receiving")
|
||||
go b.handleRocketHook(messages)
|
||||
} else {
|
||||
b.Log.Debugf("Choosing login/password based receiving")
|
||||
go b.handleRocketClient(messages)
|
||||
}
|
||||
for message := range messages {
|
||||
message.Account = b.Account
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", message)
|
||||
b.Remote <- *message
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Brocketchat) handleRocketHook(messages chan *config.Message) {
|
||||
for {
|
||||
message := b.rh.Receive()
|
||||
b.Log.Debugf("Receiving from rockethook %#v", message)
|
||||
// do not loop
|
||||
if message.UserName == b.GetString("Nick") {
|
||||
continue
|
||||
}
|
||||
messages <- &config.Message{
|
||||
UserID: message.UserID,
|
||||
Username: message.UserName,
|
||||
Text: message.Text,
|
||||
Channel: message.ChannelName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
|
||||
for message := range b.messageChan {
|
||||
// skip messages with same ID, apparently messages get duplicated for an unknown reason
|
||||
if _, ok := b.cache.Get(message.ID); ok {
|
||||
continue
|
||||
}
|
||||
b.cache.Add(message.ID, true)
|
||||
b.Log.Debugf("message %#v", message)
|
||||
m := message
|
||||
if b.skipMessage(&m) {
|
||||
b.Log.Debugf("Skipped message: %#v", message)
|
||||
continue
|
||||
}
|
||||
|
||||
rmsg := &config.Message{Text: message.Msg,
|
||||
Username: message.User.UserName,
|
||||
Channel: b.getChannelName(message.RoomID),
|
||||
Account: b.Account,
|
||||
UserID: message.User.ID,
|
||||
ID: message.ID,
|
||||
}
|
||||
messages <- rmsg
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Brocketchat) handleUploadFile(msg *config.Message) error {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if err := b.uploadFile(&fi, b.getChannelID(msg.Channel)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
198
bridge/rocketchat/helpers.go
Normal file
198
bridge/rocketchat/helpers.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package brocketchat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/42wim/matterbridge/hook/rockethook"
|
||||
"github.com/42wim/matterbridge/matterhook"
|
||||
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
|
||||
"github.com/matterbridge/Rocket.Chat.Go.SDK/realtime"
|
||||
"github.com/matterbridge/Rocket.Chat.Go.SDK/rest"
|
||||
"github.com/nelsonken/gomf"
|
||||
)
|
||||
|
||||
func (b *Brocketchat) doConnectWebhookBind() error {
|
||||
switch {
|
||||
case b.GetString("WebhookURL") != "":
|
||||
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
|
||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||
DisableServer: true})
|
||||
b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
|
||||
case b.GetString("Login") != "":
|
||||
b.Log.Info("Connecting using login/password (sending)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
b.Log.Info("Connecting using webhookbindaddress (receiving)")
|
||||
b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Brocketchat) doConnectWebhookURL() error {
|
||||
b.Log.Info("Connecting using webhookurl (sending)")
|
||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||
DisableServer: true})
|
||||
if b.GetString("Login") != "" {
|
||||
b.Log.Info("Connecting using login/password (receiving)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Brocketchat) apiLogin() error {
|
||||
b.Log.Debugf("handling apiLogin()")
|
||||
credentials := &models.UserCredentials{Email: b.GetString("login"), Password: b.GetString("password")}
|
||||
myURL, err := url.Parse(b.GetString("server"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := realtime.NewClient(myURL, b.GetBool("debug"))
|
||||
b.c = client
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
restclient := rest.NewClient(myURL, b.GetBool("debug"))
|
||||
user, err := b.c.Login(credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.user = user
|
||||
b.r = restclient
|
||||
err = b.r.Login(credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Log.Info("Connection succeeded")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Brocketchat) getChannelName(id string) string {
|
||||
b.RLock()
|
||||
defer b.RUnlock()
|
||||
if name, ok := b.channelMap[id]; ok {
|
||||
return name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *Brocketchat) getChannelID(name string) string {
|
||||
b.RLock()
|
||||
defer b.RUnlock()
|
||||
for k, v := range b.channelMap {
|
||||
if v == name || v == "#"+name {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *Brocketchat) skipMessage(message *models.Message) bool {
|
||||
return message.User.ID == b.user.ID
|
||||
}
|
||||
|
||||
func (b *Brocketchat) uploadFile(fi *config.FileInfo, channel string) error {
|
||||
fb := gomf.New()
|
||||
if err := fb.WriteField("description", fi.Comment); err != nil {
|
||||
return err
|
||||
}
|
||||
sp := strings.Split(fi.Name, ".")
|
||||
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
|
||||
if !strings.Contains(mtype, "image") && !strings.Contains(mtype, "video") {
|
||||
return nil
|
||||
}
|
||||
if err := fb.WriteFile("file", fi.Name, mtype, *fi.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := fb.GetHTTPRequest(context.TODO(), b.GetString("server")+"/api/v1/rooms.upload/"+channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("X-Auth-Token", b.user.Token)
|
||||
req.Header.Add("X-User-Id", b.user.ID)
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
b.Log.Errorf("failed: %#v", string(body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendWebhook uses the configured WebhookURL to send the message
|
||||
func (b *Brocketchat) sendWebhook(msg *config.Message) error {
|
||||
// skip events
|
||||
if msg.Event != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if b.GetBool("PrefixMessagesWithNick") {
|
||||
msg.Text = msg.Username + msg.Text
|
||||
}
|
||||
if msg.Extra != nil {
|
||||
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
|
||||
for _, rmsg := range helper.HandleExtra(msg, b.General) {
|
||||
rmsg := rmsg // scopelint
|
||||
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
|
||||
matterMessage := matterhook.OMessage{
|
||||
IconURL: iconURL,
|
||||
Channel: rmsg.Channel,
|
||||
UserName: rmsg.Username,
|
||||
Text: rmsg.Text,
|
||||
Props: make(map[string]interface{}),
|
||||
}
|
||||
if err := b.mh.Send(matterMessage); err != nil {
|
||||
b.Log.Errorf("sendWebhook failed: %s ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// webhook doesn't support file uploads, so we add the url manually
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.URL != "" {
|
||||
msg.Text += fi.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
iconURL := config.GetIconURL(msg, b.GetString("iconurl"))
|
||||
matterMessage := matterhook.OMessage{
|
||||
IconURL: iconURL,
|
||||
Channel: msg.Channel,
|
||||
UserName: msg.Username,
|
||||
Text: msg.Text,
|
||||
}
|
||||
if msg.Avatar != "" {
|
||||
matterMessage.IconURL = msg.Avatar
|
||||
}
|
||||
err := b.mh.Send(matterMessage)
|
||||
if err != nil {
|
||||
b.Log.Info(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,21 +1,47 @@
|
||||
package brocketchat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/42wim/matterbridge/hook/rockethook"
|
||||
"github.com/42wim/matterbridge/matterhook"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
|
||||
"github.com/matterbridge/Rocket.Chat.Go.SDK/realtime"
|
||||
"github.com/matterbridge/Rocket.Chat.Go.SDK/rest"
|
||||
)
|
||||
|
||||
type Brocketchat struct {
|
||||
mh *matterhook.Client
|
||||
rh *rockethook.Client
|
||||
mh *matterhook.Client
|
||||
rh *rockethook.Client
|
||||
c *realtime.Client
|
||||
r *rest.Client
|
||||
cache *lru.Cache
|
||||
*bridge.Config
|
||||
messageChan chan models.Message
|
||||
channelMap map[string]string
|
||||
user *models.User
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func New(cfg *bridge.Config) bridge.Bridger {
|
||||
return &Brocketchat{Config: cfg}
|
||||
newCache, err := lru.New(100)
|
||||
if err != nil {
|
||||
cfg.Log.Fatalf("Could not create LRU cache for rocketchat bridge: %v", err)
|
||||
}
|
||||
b := &Brocketchat{
|
||||
Config: cfg,
|
||||
messageChan: make(chan models.Message),
|
||||
channelMap: make(map[string]string),
|
||||
cache: newCache,
|
||||
}
|
||||
b.Log.Debugf("enabling rocketchat")
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Brocketchat) Command(cmd string) string {
|
||||
@@ -23,70 +49,127 @@ func (b *Brocketchat) Command(cmd string) string {
|
||||
}
|
||||
|
||||
func (b *Brocketchat) Connect() error {
|
||||
b.Log.Info("Connecting webhooks")
|
||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||
DisableServer: true})
|
||||
b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
|
||||
go b.handleRocketHook()
|
||||
if b.GetString("WebhookBindAddress") != "" {
|
||||
if err := b.doConnectWebhookBind(); err != nil {
|
||||
return err
|
||||
}
|
||||
go b.handleRocket()
|
||||
return nil
|
||||
}
|
||||
switch {
|
||||
case b.GetString("WebhookURL") != "":
|
||||
if err := b.doConnectWebhookURL(); err != nil {
|
||||
return err
|
||||
}
|
||||
go b.handleRocket()
|
||||
return nil
|
||||
case b.GetString("Login") != "":
|
||||
b.Log.Info("Connecting using login/password (sending and receiving)")
|
||||
err := b.apiLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go b.handleRocket()
|
||||
}
|
||||
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" &&
|
||||
b.GetString("Login") == "" {
|
||||
return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Login/Password/Server configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Brocketchat) Disconnect() error {
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
|
||||
if b.c == nil {
|
||||
return nil
|
||||
}
|
||||
id, err := b.c.GetChannelId(strings.TrimPrefix(channel.Name, "#"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Lock()
|
||||
b.channelMap[id] = channel.Name
|
||||
b.Unlock()
|
||||
mychannel := &models.Channel{ID: id, Name: strings.TrimPrefix(channel.Name, "#")}
|
||||
if err := b.c.JoinChannel(id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.c.SubscribeToMessageStream(mychannel, b.messageChan); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Brocketchat) Send(msg config.Message) (string, error) {
|
||||
// ignore delete messages
|
||||
if msg.Event == config.EventMsgDelete {
|
||||
return "", nil
|
||||
// strip the # if people has set this
|
||||
msg.Channel = strings.TrimPrefix(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 + "_"
|
||||
}
|
||||
b.Log.Debugf("=> Receiving %#v", msg)
|
||||
|
||||
// Delete message
|
||||
if msg.Event == config.EventMsgDelete {
|
||||
if msg.ID == "" {
|
||||
return "", nil
|
||||
}
|
||||
return msg.ID, b.c.DeleteMessage(&models.Message{ID: msg.ID})
|
||||
}
|
||||
|
||||
// Use webhook to send the message
|
||||
if b.GetString("WebhookURL") != "" {
|
||||
return "", b.sendWebhook(&msg)
|
||||
}
|
||||
|
||||
// Prepend nick if configured
|
||||
if b.GetBool("PrefixMessagesWithNick") {
|
||||
msg.Text = msg.Username + msg.Text
|
||||
}
|
||||
|
||||
// Edit message if we have an ID
|
||||
if msg.ID != "" {
|
||||
return msg.ID, b.c.EditMessage(&models.Message{ID: msg.ID, Msg: msg.Text, RoomID: b.getChannelID(msg.Channel)})
|
||||
}
|
||||
|
||||
// Upload a file if it exists
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
rmsg := rmsg // scopelint
|
||||
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
|
||||
matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: rmsg.Channel, UserName: rmsg.Username, Text: rmsg.Text}
|
||||
b.mh.Send(matterMessage)
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.URL != "" {
|
||||
msg.Text += fi.URL
|
||||
}
|
||||
// strip the # if people has set this
|
||||
rmsg.Channel = strings.TrimPrefix(rmsg.Channel, "#")
|
||||
smsg := &models.Message{
|
||||
RoomID: b.getChannelID(rmsg.Channel),
|
||||
Msg: rmsg.Username + rmsg.Text,
|
||||
PostMessage: models.PostMessage{
|
||||
Avatar: rmsg.Avatar,
|
||||
Alias: rmsg.Username,
|
||||
},
|
||||
}
|
||||
if _, err := b.c.SendMessage(smsg); err != nil {
|
||||
b.Log.Errorf("SendMessage failed: %s", err)
|
||||
}
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
return "", b.handleUploadFile(&msg)
|
||||
}
|
||||
}
|
||||
|
||||
iconURL := config.GetIconURL(&msg, b.GetString("iconurl"))
|
||||
matterMessage := matterhook.OMessage{IconURL: iconURL}
|
||||
matterMessage.Channel = msg.Channel
|
||||
matterMessage.UserName = msg.Username
|
||||
matterMessage.Type = ""
|
||||
matterMessage.Text = msg.Text
|
||||
err := b.mh.Send(matterMessage)
|
||||
if err != nil {
|
||||
b.Log.Info(err)
|
||||
smsg := &models.Message{
|
||||
RoomID: channel.ID,
|
||||
Msg: msg.Text,
|
||||
PostMessage: models.PostMessage{
|
||||
Avatar: msg.Avatar,
|
||||
Alias: msg.Username,
|
||||
},
|
||||
}
|
||||
|
||||
rmsg, err := b.c.SendMessage(smsg)
|
||||
if rmsg == nil {
|
||||
return "", err
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (b *Brocketchat) handleRocketHook() {
|
||||
for {
|
||||
message := b.rh.Receive()
|
||||
b.Log.Debugf("Receiving from rockethook %#v", message)
|
||||
// do not loop
|
||||
if message.UserName == b.GetString("Nick") {
|
||||
continue
|
||||
}
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.UserName, b.Account)
|
||||
b.Remote <- config.Message{Text: message.Text, Username: message.UserName, Channel: message.ChannelName, Account: b.Account, UserID: message.UserID}
|
||||
}
|
||||
return rmsg.ID, err
|
||||
}
|
||||
|
||||
@@ -22,20 +22,20 @@ func (b *Bslack) handleSlack() {
|
||||
time.Sleep(time.Second)
|
||||
b.Log.Debug("Start listening for Slack 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)
|
||||
// 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.Remote <- *message
|
||||
}
|
||||
@@ -75,21 +75,21 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
|
||||
// When we join a channel we update the full list of users as
|
||||
// well as the information for the channel that we joined as this
|
||||
// should now tell that we are a member of it.
|
||||
b.populateUsers()
|
||||
|
||||
b.channelsMutex.Lock()
|
||||
b.channelsByID[ev.Channel.ID] = &ev.Channel
|
||||
b.channelsByName[ev.Channel.Name] = &ev.Channel
|
||||
b.channelsMutex.Unlock()
|
||||
b.channels.registerChannel(ev.Channel)
|
||||
case *slack.ConnectedEvent:
|
||||
b.si = ev.Info
|
||||
b.populateChannels()
|
||||
b.populateUsers()
|
||||
b.channels.populateChannels(true)
|
||||
b.users.populateUsers(true)
|
||||
case *slack.InvalidAuthEvent:
|
||||
b.Log.Fatalf("Invalid Token %#v", ev)
|
||||
case *slack.ConnectionErrorEvent:
|
||||
b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
|
||||
case *slack.MemberJoinedChannelEvent:
|
||||
b.users.populateUser(ev.User)
|
||||
case *slack.LatencyReport:
|
||||
continue
|
||||
default:
|
||||
b.Log.Debugf("Unhandled incoming event: %T", ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,11 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
|
||||
return b.GetBool(noSendJoinConfig)
|
||||
case sPinnedItem, sUnpinnedItem:
|
||||
return true
|
||||
case sChannelTopic, sChannelPurpose:
|
||||
// Skip the event if our bot/user account changed the topic/purpose
|
||||
if ev.User == b.si.User.ID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Skip any messages that we made ourselves or from 'slackbot' (see #527).
|
||||
@@ -125,18 +130,23 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// It seems ev.SubMessage.Edited == nil when slack unfurls.
|
||||
// Do not forward these messages. See Github issue #266.
|
||||
if ev.SubMessage != nil &&
|
||||
ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp &&
|
||||
ev.SubMessage.Edited == nil {
|
||||
return true
|
||||
if ev.SubMessage != nil {
|
||||
// It seems ev.SubMessage.Edited == nil when slack unfurls.
|
||||
// Do not forward these messages. See Github issue #266.
|
||||
if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp &&
|
||||
ev.SubMessage.Edited == nil {
|
||||
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 {
|
||||
return b.filesCached(ev.Files)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -185,6 +195,9 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
|
||||
// This is probably a webhook we couldn't resolve.
|
||||
return nil, fmt.Errorf("message handling resulted in an empty bot message (probably an incoming webhook we couldn't resolve): %#v", ev)
|
||||
}
|
||||
if ev.SubMessage != nil {
|
||||
return nil, fmt.Errorf("message handling resulted in an empty message: %#v with submessage %#v", ev, ev.SubMessage)
|
||||
}
|
||||
return nil, fmt.Errorf("message handling resulted in an empty message: %#v", ev)
|
||||
}
|
||||
return rmsg, nil
|
||||
@@ -193,7 +206,6 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
|
||||
func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
|
||||
switch ev.SubType {
|
||||
case sChannelJoined, sMemberJoined:
|
||||
b.populateUsers()
|
||||
// There's no further processing needed on channel events
|
||||
// so we return 'true'.
|
||||
return true
|
||||
@@ -201,7 +213,15 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
|
||||
rmsg.Username = sSystemUser
|
||||
rmsg.Event = config.EventJoinLeave
|
||||
case sChannelTopic, sChannelPurpose:
|
||||
b.channels.populateChannels(false)
|
||||
rmsg.Event = config.EventTopicChange
|
||||
case sMessageChanged:
|
||||
rmsg.Text = ev.SubMessage.Text
|
||||
// handle deleted thread starting messages
|
||||
if ev.SubMessage.Text == "This message was deleted." {
|
||||
rmsg.Event = config.EventMsgDelete
|
||||
return true
|
||||
}
|
||||
case sMessageDeleted:
|
||||
rmsg.Text = config.EventMsgDelete
|
||||
rmsg.Event = config.EventMsgDelete
|
||||
@@ -242,14 +262,14 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
|
||||
|
||||
// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
|
||||
for i := range ev.Files {
|
||||
if err := b.handleDownloadFile(rmsg, &ev.Files[i]); err != nil {
|
||||
if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
|
||||
b.Log.Errorf("Could not download incoming file: %#v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) {
|
||||
channelInfo, err := b.getChannelByID(ev.Channel)
|
||||
channelInfo, err := b.channels.getChannelByID(ev.Channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -261,7 +281,7 @@ func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message,
|
||||
}
|
||||
|
||||
// handleDownloadFile handles file download
|
||||
func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File) error {
|
||||
func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retry bool) error {
|
||||
if b.fileCached(file) {
|
||||
return nil
|
||||
}
|
||||
@@ -277,6 +297,12 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File) erro
|
||||
return fmt.Errorf("download %s failed %#v", file.URLPrivateDownload, err)
|
||||
}
|
||||
|
||||
if len(*data) != file.Size && !retry {
|
||||
b.Log.Debugf("Data size (%d) is not equal to size declared (%d)\n", len(*data), file.Size)
|
||||
time.Sleep(1 * time.Second)
|
||||
return b.handleDownloadFile(rmsg, file, true)
|
||||
}
|
||||
|
||||
// If a comment is attached to the file(s) it is in the 'Text' field of the Slack messge event
|
||||
// and should be added as comment to only one of the files. We reset the 'Text' field to ensure
|
||||
// that the comment is not duplicated.
|
||||
@@ -286,6 +312,29 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleGetChannelMembers handles messages containing the GetChannelMembers event
|
||||
// Sends a message to the router containing *config.ChannelMembers
|
||||
func (b *Bslack) handleGetChannelMembers(rmsg *config.Message) bool {
|
||||
if rmsg.Event != config.EventGetChannelMembers {
|
||||
return false
|
||||
}
|
||||
|
||||
cMembers := b.channels.getChannelMembers(b.users)
|
||||
|
||||
extra := make(map[string][]interface{})
|
||||
extra[config.EventGetChannelMembers] = append(extra[config.EventGetChannelMembers], cMembers)
|
||||
msg := config.Message{
|
||||
Extra: extra,
|
||||
Event: config.EventGetChannelMembers,
|
||||
Account: b.Account,
|
||||
}
|
||||
|
||||
b.Log.Debugf("sending msg to remote %#v", msg)
|
||||
b.Remote <- msg
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// fileCached implements Matterbridge's caching logic for files
|
||||
// shared via Slack.
|
||||
//
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package bslack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -9,162 +8,14 @@ import (
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/nlopes/slack"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (b *Bslack) getUser(id string) *slack.User {
|
||||
b.usersMutex.RLock()
|
||||
defer b.usersMutex.RUnlock()
|
||||
|
||||
return b.users[id]
|
||||
}
|
||||
|
||||
func (b *Bslack) getUsername(id string) string {
|
||||
if user := b.getUser(id); user != nil {
|
||||
if user.Profile.DisplayName != "" {
|
||||
return user.Profile.DisplayName
|
||||
}
|
||||
return user.Name
|
||||
}
|
||||
b.Log.Warnf("Could not find user with ID '%s'", id)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *Bslack) getAvatar(id string) string {
|
||||
if user := b.getUser(id); user != nil {
|
||||
return user.Profile.Image48
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *Bslack) getChannel(channel string) (*slack.Channel, error) {
|
||||
if strings.HasPrefix(channel, "ID:") {
|
||||
return b.getChannelByID(strings.TrimPrefix(channel, "ID:"))
|
||||
}
|
||||
return b.getChannelByName(channel)
|
||||
}
|
||||
|
||||
func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) {
|
||||
b.channelsMutex.RLock()
|
||||
defer b.channelsMutex.RUnlock()
|
||||
|
||||
if channel, ok := b.channelsByName[name]; ok {
|
||||
return channel, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%s: channel %s not found", b.Account, name)
|
||||
}
|
||||
|
||||
func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) {
|
||||
b.channelsMutex.RLock()
|
||||
defer b.channelsMutex.RUnlock()
|
||||
|
||||
if channel, ok := b.channelsByID[ID]; ok {
|
||||
return channel, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%s: channel %s not found", b.Account, ID)
|
||||
}
|
||||
|
||||
const minimumRefreshInterval = 10 * time.Second
|
||||
|
||||
func (b *Bslack) populateUsers() {
|
||||
b.refreshMutex.Lock()
|
||||
if time.Now().Before(b.earliestUserRefresh) || b.refreshInProgress {
|
||||
b.Log.Debugf("Not refreshing user list as it was done less than %v ago.",
|
||||
minimumRefreshInterval)
|
||||
b.refreshMutex.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
b.refreshInProgress = true
|
||||
b.refreshMutex.Unlock()
|
||||
|
||||
newUsers := map[string]*slack.User{}
|
||||
pagination := b.sc.GetUsersPaginated(slack.GetUsersOptionLimit(200))
|
||||
for {
|
||||
var err error
|
||||
pagination, err = pagination.Next(context.Background())
|
||||
if err != nil {
|
||||
if pagination.Done(err) {
|
||||
break
|
||||
}
|
||||
|
||||
if err = b.handleRateLimit(err); err != nil {
|
||||
b.Log.Errorf("Could not retrieve users: %#v", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range pagination.Users {
|
||||
newUsers[pagination.Users[i].ID] = &pagination.Users[i]
|
||||
}
|
||||
}
|
||||
|
||||
b.usersMutex.Lock()
|
||||
defer b.usersMutex.Unlock()
|
||||
b.users = newUsers
|
||||
|
||||
b.refreshMutex.Lock()
|
||||
defer b.refreshMutex.Unlock()
|
||||
b.earliestUserRefresh = time.Now().Add(minimumRefreshInterval)
|
||||
b.refreshInProgress = false
|
||||
}
|
||||
|
||||
func (b *Bslack) populateChannels() {
|
||||
b.refreshMutex.Lock()
|
||||
if time.Now().Before(b.earliestChannelRefresh) || b.refreshInProgress {
|
||||
b.Log.Debugf("Not refreshing channel list as it was done less than %v seconds ago.",
|
||||
minimumRefreshInterval)
|
||||
b.refreshMutex.Unlock()
|
||||
return
|
||||
}
|
||||
b.refreshInProgress = true
|
||||
b.refreshMutex.Unlock()
|
||||
|
||||
newChannelsByID := map[string]*slack.Channel{}
|
||||
newChannelsByName := map[string]*slack.Channel{}
|
||||
|
||||
// We only retrieve public and private channels, not IMs
|
||||
// and MPIMs as those do not have a channel name.
|
||||
queryParams := &slack.GetConversationsParameters{
|
||||
ExcludeArchived: "true",
|
||||
Types: []string{"public_channel,private_channel"},
|
||||
}
|
||||
for {
|
||||
channels, nextCursor, err := b.sc.GetConversations(queryParams)
|
||||
if err != nil {
|
||||
if err = b.handleRateLimit(err); err != nil {
|
||||
b.Log.Errorf("Could not retrieve channels: %#v", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range channels {
|
||||
newChannelsByID[channels[i].ID] = &channels[i]
|
||||
newChannelsByName[channels[i].Name] = &channels[i]
|
||||
}
|
||||
if nextCursor == "" {
|
||||
break
|
||||
}
|
||||
queryParams.Cursor = nextCursor
|
||||
}
|
||||
|
||||
b.channelsMutex.Lock()
|
||||
defer b.channelsMutex.Unlock()
|
||||
b.channelsByID = newChannelsByID
|
||||
b.channelsByName = newChannelsByName
|
||||
|
||||
b.refreshMutex.Lock()
|
||||
defer b.refreshMutex.Unlock()
|
||||
b.earliestChannelRefresh = time.Now().Add(minimumRefreshInterval)
|
||||
b.refreshInProgress = false
|
||||
}
|
||||
|
||||
// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
|
||||
// router before we apply message-dependent modifications.
|
||||
func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) {
|
||||
// Use our own func because rtm.GetChannelInfo doesn't work for private channels.
|
||||
channel, err := b.getChannelByID(ev.Channel)
|
||||
channel, err := b.channels.getChannelByID(ev.Channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -191,6 +42,13 @@ func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Messag
|
||||
}
|
||||
}
|
||||
|
||||
// For edits, only submessage has thread ts.
|
||||
// Ensures edits to threaded messages maintain their prefix hint on the
|
||||
// unthreaded end.
|
||||
if ev.SubMessage != nil {
|
||||
rmsg.ParentID = ev.SubMessage.ThreadTimestamp
|
||||
}
|
||||
|
||||
if err = b.populateMessageWithUserInfo(ev, rmsg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -219,7 +77,7 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi
|
||||
return nil
|
||||
}
|
||||
|
||||
user := b.getUser(userID)
|
||||
user := b.users.getUser(userID)
|
||||
if user == nil {
|
||||
return fmt.Errorf("could not find information for user with id %s", ev.User)
|
||||
}
|
||||
@@ -245,13 +103,14 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
|
||||
break
|
||||
}
|
||||
|
||||
if err = b.handleRateLimit(err); err != nil {
|
||||
if err = handleRateLimit(b.Log, err); err != nil {
|
||||
b.Log.Errorf("Could not retrieve bot information: %#v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
b.Log.Debugf("Found bot %#v", bot)
|
||||
|
||||
if bot.Name != "" && bot.Name != "Slack API Tester" {
|
||||
if bot.Name != "" {
|
||||
rmsg.Username = bot.Name
|
||||
if ev.Username != "" {
|
||||
rmsg.Username = ev.Username
|
||||
@@ -262,17 +121,34 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
|
||||
}
|
||||
|
||||
var (
|
||||
mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
|
||||
channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
|
||||
variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
|
||||
urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`)
|
||||
mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
|
||||
channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
|
||||
variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
|
||||
urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`)
|
||||
codeFenceRE = regexp.MustCompile(`(?m)^` + "```" + `\w+$`)
|
||||
topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`)
|
||||
)
|
||||
|
||||
func (b *Bslack) extractTopicOrPurpose(text string) (string, string) {
|
||||
r := topicOrPurposeRE.FindStringSubmatch(text)
|
||||
if len(r) == 5 {
|
||||
action, updateType, extracted := r[2], r[3], r[4]
|
||||
switch action {
|
||||
case "set":
|
||||
return updateType, extracted
|
||||
case "cleared":
|
||||
return updateType, ""
|
||||
}
|
||||
}
|
||||
b.Log.Warnf("Encountered channel topic or purpose change message with unexpected format: %s", text)
|
||||
return "unknown", ""
|
||||
}
|
||||
|
||||
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
|
||||
func (b *Bslack) replaceMention(text string) string {
|
||||
replaceFunc := func(match string) string {
|
||||
userID := strings.Trim(match, "@<>")
|
||||
if username := b.getUsername(userID); userID != "" {
|
||||
if username := b.users.getUsername(userID); userID != "" {
|
||||
return "@" + username
|
||||
}
|
||||
return match
|
||||
@@ -312,12 +188,42 @@ func (b *Bslack) replaceURL(text string) string {
|
||||
return text
|
||||
}
|
||||
|
||||
func (b *Bslack) handleRateLimit(err error) error {
|
||||
func (b *Bslack) replaceCodeFence(text string) string {
|
||||
return codeFenceRE.ReplaceAllString(text, "```")
|
||||
}
|
||||
|
||||
// getUsersInConversation returns an array of userIDs that are members of channelID
|
||||
func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
|
||||
channelMembers := []string{}
|
||||
for {
|
||||
queryParams := &slack.GetUsersInConversationParameters{
|
||||
ChannelID: channelID,
|
||||
}
|
||||
|
||||
members, nextCursor, err := b.sc.GetUsersInConversation(queryParams)
|
||||
if err != nil {
|
||||
if err = handleRateLimit(b.Log, err); err != nil {
|
||||
return channelMembers, fmt.Errorf("Could not retrieve users in channels: %#v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
channelMembers = append(channelMembers, members...)
|
||||
|
||||
if nextCursor == "" {
|
||||
break
|
||||
}
|
||||
queryParams.Cursor = nextCursor
|
||||
}
|
||||
return channelMembers, nil
|
||||
}
|
||||
|
||||
func handleRateLimit(log *logrus.Entry, err error) error {
|
||||
rateLimit, ok := err.(*slack.RateLimitedError)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
b.Log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
|
||||
log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
|
||||
time.Sleep(rateLimit.RetryAfter)
|
||||
return nil
|
||||
}
|
||||
|
||||
36
bridge/slack/helpers_test.go
Normal file
36
bridge/slack/helpers_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package bslack
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExtractTopicOrPurpose(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
input string
|
||||
wantChangeType string
|
||||
wantOutput string
|
||||
}{
|
||||
"success - topic type": {"@someone set channel topic: foo bar", "topic", "foo bar"},
|
||||
"success - purpose type": {"@someone set channel purpose: foo bar", "purpose", "foo bar"},
|
||||
"success - one line": {"@someone set channel topic: foo bar", "topic", "foo bar"},
|
||||
"success - multi-line": {"@someone set channel topic: foo\nbar", "topic", "foo\nbar"},
|
||||
"success - cleared": {"@someone cleared channel topic", "topic", ""},
|
||||
"error - unhandled": {"some unmatched message", "unknown", ""},
|
||||
}
|
||||
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(ioutil.Discard)
|
||||
cfg := &bridge.Config{Bridge: &bridge.Bridge{Log: logrus.NewEntry(logger)}}
|
||||
b := newBridge(cfg)
|
||||
for name, tc := range testcases {
|
||||
gotChangeType, gotOutput := b.extractTopicOrPurpose(tc.input)
|
||||
|
||||
assert.Equalf(t, tc.wantChangeType, gotChangeType, "This testcase failed: %s", name)
|
||||
assert.Equalf(t, tc.wantOutput, gotOutput, "This testcase failed: %s", name)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ type BLegacy struct {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -55,14 +57,18 @@ func (b *BLegacy) Connect() error {
|
||||
})
|
||||
if b.GetString(tokenConfig) != "" {
|
||||
b.Log.Info("Connecting using token (receiving)")
|
||||
b.sc = slack.New(b.GetString(tokenConfig))
|
||||
b.sc = slack.New(b.GetString(tokenConfig), slack.OptionDebug(b.GetBool("debug")))
|
||||
b.channels = newChannelManager(b.Log, b.sc)
|
||||
b.users = newUserManager(b.Log, b.sc)
|
||||
b.rtm = b.sc.NewRTM()
|
||||
go b.rtm.ManageConnection()
|
||||
go b.handleSlack()
|
||||
}
|
||||
} else if b.GetString(tokenConfig) != "" {
|
||||
b.Log.Info("Connecting using token (sending and receiving)")
|
||||
b.sc = slack.New(b.GetString(tokenConfig))
|
||||
b.sc = slack.New(b.GetString(tokenConfig), slack.OptionDebug(b.GetBool("debug")))
|
||||
b.channels = newChannelManager(b.Log, b.sc)
|
||||
b.users = newUserManager(b.Log, b.sc)
|
||||
b.rtm = b.sc.NewRTM()
|
||||
go b.rtm.ManageConnection()
|
||||
go b.handleSlack()
|
||||
|
||||
@@ -30,17 +30,9 @@ type Bslack struct {
|
||||
uuid string
|
||||
useChannelID bool
|
||||
|
||||
users map[string]*slack.User
|
||||
usersMutex sync.RWMutex
|
||||
|
||||
channelsByID map[string]*slack.Channel
|
||||
channelsByName map[string]*slack.Channel
|
||||
channelsMutex sync.RWMutex
|
||||
|
||||
refreshInProgress bool
|
||||
earliestChannelRefresh time.Time
|
||||
earliestUserRefresh time.Time
|
||||
refreshMutex sync.Mutex
|
||||
channels *channels
|
||||
users *users
|
||||
legacy bool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -48,6 +40,7 @@ const (
|
||||
sChannelLeave = "channel_leave"
|
||||
sChannelJoined = "channel_joined"
|
||||
sMemberJoined = "member_joined_channel"
|
||||
sMessageChanged = "message_changed"
|
||||
sMessageDeleted = "message_deleted"
|
||||
sSlackAttachment = "slack_attachment"
|
||||
sPinnedItem = "pinned_item"
|
||||
@@ -76,14 +69,9 @@ func New(cfg *bridge.Config) bridge.Bridger {
|
||||
// Print a deprecation warning for legacy non-bot tokens (#527).
|
||||
token := cfg.GetString(tokenConfig)
|
||||
if token != "" && !strings.HasPrefix(token, "xoxb") {
|
||||
cfg.Log.Error("Non-bot token detected. It is STRONGLY recommended to use a proper bot-token instead.")
|
||||
cfg.Log.Error("Legacy tokens may be deprecated by Slack at short notice. See the Matterbridge GitHub wiki for a migration guide.")
|
||||
cfg.Log.Error("See https://github.com/42wim/matterbridge/wiki/Slack-bot-setup")
|
||||
cfg.Log.Error("")
|
||||
cfg.Log.Error("To continue using a legacy token please move your configuration to a \"slack-legacy\" bridge instead.")
|
||||
cfg.Log.Error("See https://github.com/42wim/matterbridge/wiki/Section-Slack-(basic)#legacy-configuration)")
|
||||
cfg.Log.Error("Delaying start of bridge by 30 seconds. Future Matterbridge release will fail here unless you use a \"slack-legacy\" bridge.")
|
||||
time.Sleep(30 * time.Second)
|
||||
cfg.Log.Warn("Non-bot token detected. It is STRONGLY recommended to use a proper bot-token instead.")
|
||||
cfg.Log.Warn("Legacy tokens may be deprecated by Slack at short notice. See the Matterbridge GitHub wiki for a migration guide.")
|
||||
cfg.Log.Warn("See https://github.com/42wim/matterbridge/wiki/Slack-bot-setup")
|
||||
return NewLegacy(cfg)
|
||||
}
|
||||
return newBridge(cfg)
|
||||
@@ -95,14 +83,9 @@ func newBridge(cfg *bridge.Config) *Bslack {
|
||||
cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err)
|
||||
}
|
||||
b := &Bslack{
|
||||
Config: cfg,
|
||||
uuid: xid.New().String(),
|
||||
cache: newCache,
|
||||
users: map[string]*slack.User{},
|
||||
channelsByID: map[string]*slack.Channel{},
|
||||
channelsByName: map[string]*slack.Channel{},
|
||||
earliestChannelRefresh: time.Now(),
|
||||
earliestUserRefresh: time.Now(),
|
||||
Config: cfg,
|
||||
uuid: xid.New().String(),
|
||||
cache: newCache,
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -122,7 +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 token := b.GetString(tokenConfig); token != "" {
|
||||
b.Log.Info("Connecting using token")
|
||||
b.sc = slack.New(token)
|
||||
|
||||
b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug")))
|
||||
|
||||
b.channels = newChannelManager(b.Log, b.sc)
|
||||
b.users = newUserManager(b.Log, b.sc)
|
||||
|
||||
b.rtm = b.sc.NewRTM()
|
||||
go b.rtm.ManageConnection()
|
||||
go b.handleSlack()
|
||||
@@ -164,9 +152,21 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.populateChannels()
|
||||
// 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 {
|
||||
return fmt.Errorf("could not join channel: %#v", err)
|
||||
}
|
||||
@@ -176,7 +176,8 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
|
||||
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 nil
|
||||
@@ -192,6 +193,8 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
|
||||
b.Log.Debugf("=> Receiving %#v", msg)
|
||||
}
|
||||
|
||||
msg.Text = b.replaceCodeFence(msg.Text)
|
||||
|
||||
// Make a action /me of the message
|
||||
if msg.Event == config.EventUserAction {
|
||||
msg.Text = "_" + msg.Text + "_"
|
||||
@@ -199,16 +202,16 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
|
||||
|
||||
// Use webhook to send the message
|
||||
if b.GetString(outgoingWebhookConfig) != "" {
|
||||
return b.sendWebhook(msg)
|
||||
return "", b.sendWebhook(msg)
|
||||
}
|
||||
return b.sendRTM(msg)
|
||||
}
|
||||
|
||||
// sendWebhook uses the configured WebhookURL to send the message
|
||||
func (b *Bslack) sendWebhook(msg config.Message) (string, error) {
|
||||
func (b *Bslack) sendWebhook(msg config.Message) error {
|
||||
// Skip events.
|
||||
if msg.Event != "" {
|
||||
return "", nil
|
||||
return nil
|
||||
}
|
||||
|
||||
if b.GetBool(useNickPrefixConfig) {
|
||||
@@ -263,13 +266,18 @@ func (b *Bslack) sendWebhook(msg config.Message) (string, error) {
|
||||
}
|
||||
if err := b.mh.Send(matterMessage); err != nil {
|
||||
b.Log.Errorf("Failed to send message via webhook: %#v", err)
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
return "", nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bslack) sendRTM(msg config.Message) (string, error) {
|
||||
channelInfo, err := b.getChannel(msg.Channel)
|
||||
// Handle channelmember messages.
|
||||
if handled := b.handleGetChannelMembers(&msg); handled {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
channelInfo, err := b.channels.getChannel(msg.Channel)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not send message: %v", err)
|
||||
}
|
||||
@@ -280,8 +288,20 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Handle message deletions.
|
||||
var handled bool
|
||||
|
||||
// Handle topic/purpose updates.
|
||||
if handled, err = b.handleTopicOrPurpose(&msg, channelInfo); handled {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Handle prefix hint for unthreaded messages.
|
||||
if msg.ParentID == "msg-parent-not-found" {
|
||||
msg.ParentID = ""
|
||||
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
|
||||
}
|
||||
|
||||
// Handle message deletions.
|
||||
if handled, err = b.deleteMessage(&msg, channelInfo); handled {
|
||||
return msg.ID, err
|
||||
}
|
||||
@@ -296,12 +316,13 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
|
||||
return msg.ID, err
|
||||
}
|
||||
|
||||
messageParameters := b.prepareMessageParameters(&msg)
|
||||
|
||||
// Upload a file if it exists.
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
_, _, err = b.rtm.PostMessage(channelInfo.ID, rmsg.Username+rmsg.Text, *messageParameters)
|
||||
extraMsgs := helper.HandleExtra(&msg, b.General)
|
||||
for i := range extraMsgs {
|
||||
rmsg := &extraMsgs[i]
|
||||
rmsg.Text = rmsg.Username + rmsg.Text
|
||||
_, err = b.postMessage(rmsg, channelInfo)
|
||||
if err != nil {
|
||||
b.Log.Error(err)
|
||||
}
|
||||
@@ -311,7 +332,50 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
|
||||
}
|
||||
|
||||
// Post message.
|
||||
return b.postMessage(&msg, messageParameters, channelInfo)
|
||||
return b.postMessage(&msg, channelInfo)
|
||||
}
|
||||
|
||||
func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) error {
|
||||
var updateFunc func(channelID string, value string) (*slack.Channel, error)
|
||||
|
||||
incomingChangeType, text := b.extractTopicOrPurpose(msg.Text)
|
||||
switch incomingChangeType {
|
||||
case "topic":
|
||||
updateFunc = b.rtm.SetTopicOfConversation
|
||||
case "purpose":
|
||||
updateFunc = b.rtm.SetPurposeOfConversation
|
||||
default:
|
||||
b.Log.Errorf("Unhandled type received from extractTopicOrPurpose: %s", incomingChangeType)
|
||||
return nil
|
||||
}
|
||||
for {
|
||||
_, err := updateFunc(channelInfo.ID, text)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if err = handleRateLimit(b.Log, err); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handles updating topic/purpose and determining whether to further propagate update messages.
|
||||
func (b *Bslack) handleTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
|
||||
if msg.Event != config.EventTopicChange {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if b.GetBool("SyncTopic") {
|
||||
return true, b.updateTopicOrPurpose(msg, channelInfo)
|
||||
}
|
||||
|
||||
// Pass along to normal message handlers.
|
||||
if b.GetBool("ShowTopicChange") {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Swallow message as handled no-op.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
|
||||
@@ -330,7 +394,7 @@ func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err = b.handleRateLimit(err); err != nil {
|
||||
if err = handleRateLimit(b.Log, err); err != nil {
|
||||
b.Log.Errorf("Failed to delete user message from Slack: %#v", err)
|
||||
return true, err
|
||||
}
|
||||
@@ -341,28 +405,39 @@ func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (b
|
||||
if msg.ID == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
messageOptions := b.prepareMessageOptions(msg)
|
||||
for {
|
||||
_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, msg.Text)
|
||||
messageOptions = append(messageOptions, slack.MsgOptionText(msg.Text, false))
|
||||
_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err = b.handleRateLimit(err); err != nil {
|
||||
if err = handleRateLimit(b.Log, err); err != nil {
|
||||
b.Log.Errorf("Failed to edit user message on Slack: %#v", err)
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bslack) postMessage(msg *config.Message, messageParameters *slack.PostMessageParameters, channelInfo *slack.Channel) (string, error) {
|
||||
func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (string, error) {
|
||||
// don't post empty messages
|
||||
if msg.Text == "" {
|
||||
return "", nil
|
||||
}
|
||||
messageOptions := b.prepareMessageOptions(msg)
|
||||
messageOptions = append(
|
||||
messageOptions,
|
||||
slack.MsgOptionText(msg.Text, false),
|
||||
slack.MsgOptionEnableLinkUnfurl(),
|
||||
)
|
||||
for {
|
||||
_, id, err := b.rtm.PostMessage(channelInfo.ID, msg.Text, *messageParameters)
|
||||
_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
|
||||
if err == nil {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
if err = b.handleRateLimit(err); err != nil {
|
||||
if err = handleRateLimit(b.Log, err); err != nil {
|
||||
b.Log.Errorf("Failed to sent user message to Slack: %#v", err)
|
||||
return "", err
|
||||
}
|
||||
@@ -385,11 +460,16 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
|
||||
ts := time.Now()
|
||||
b.Log.Debugf("Adding file %s to cache at %s with timestamp", fi.Name, ts.String())
|
||||
b.cache.Add("filename"+fi.Name, ts)
|
||||
initialComment := fmt.Sprintf("File from %s", msg.Username)
|
||||
if fi.Comment != "" {
|
||||
initialComment += fmt.Sprintf("with comment: %s", fi.Comment)
|
||||
}
|
||||
res, err := b.sc.UploadFile(slack.FileUploadParameters{
|
||||
Reader: bytes.NewReader(*fi.Data),
|
||||
Filename: fi.Name,
|
||||
Channels: []string{channelID},
|
||||
InitialComment: fi.Comment,
|
||||
Reader: bytes.NewReader(*fi.Data),
|
||||
Filename: fi.Name,
|
||||
Channels: []string{channelID},
|
||||
InitialComment: initialComment,
|
||||
ThreadTimestamp: msg.ParentID,
|
||||
})
|
||||
if err != nil {
|
||||
b.Log.Errorf("uploadfile %#v", err)
|
||||
@@ -402,7 +482,7 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bslack) prepareMessageParameters(msg *config.Message) *slack.PostMessageParameters {
|
||||
func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
|
||||
params := slack.NewPostMessageParameters()
|
||||
if b.GetBool(useNickPrefixConfig) {
|
||||
params.AsUser = true
|
||||
@@ -414,17 +494,23 @@ func (b *Bslack) prepareMessageParameters(msg *config.Message) *slack.PostMessag
|
||||
if msg.Avatar != "" {
|
||||
params.IconURL = msg.Avatar
|
||||
}
|
||||
|
||||
var attachments []slack.Attachment
|
||||
// add a callback ID so we can see we created it
|
||||
params.Attachments = append(params.Attachments, slack.Attachment{CallbackID: "matterbridge_" + b.uuid})
|
||||
attachments = append(attachments, slack.Attachment{CallbackID: "matterbridge_" + b.uuid})
|
||||
// add file attachments
|
||||
params.Attachments = append(params.Attachments, b.createAttach(msg.Extra)...)
|
||||
attachments = append(attachments, b.createAttach(msg.Extra)...)
|
||||
// add slack attachments (from another slack bridge)
|
||||
if msg.Extra != nil {
|
||||
for _, attach := range msg.Extra[sSlackAttachment] {
|
||||
params.Attachments = append(params.Attachments, attach.([]slack.Attachment)...)
|
||||
attachments = append(attachments, attach.([]slack.Attachment)...)
|
||||
}
|
||||
}
|
||||
return ¶ms
|
||||
|
||||
var opts []slack.MsgOption
|
||||
opts = append(opts, slack.MsgOptionAttachments(attachments...))
|
||||
opts = append(opts, slack.MsgOptionPostMessageParameters(params))
|
||||
return opts
|
||||
}
|
||||
|
||||
func (b *Bslack) createAttach(extra map[string][]interface{}) []slack.Attachment {
|
||||
|
||||
336
bridge/slack/users_channels.go
Normal file
336
bridge/slack/users_channels.go
Normal 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
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
"github.com/shazow/ssh-chat/sshd"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Bsshchat struct {
|
||||
@@ -23,21 +22,35 @@ func New(cfg *bridge.Config) bridge.Bridger {
|
||||
}
|
||||
|
||||
func (b *Bsshchat) Connect() error {
|
||||
var err error
|
||||
b.Log.Infof("Connecting %s", b.GetString("Server"))
|
||||
|
||||
// connHandler will be called by 'sshd.ConnectShell()' below
|
||||
// once the connection is established in order to handle it.
|
||||
connErr := make(chan error, 1) // Needs to be buffered.
|
||||
connSignal := make(chan struct{})
|
||||
connHandler := func(r io.Reader, w io.WriteCloser) error {
|
||||
b.r = bufio.NewScanner(r)
|
||||
b.r.Scan()
|
||||
b.w = w
|
||||
if _, err := b.w.Write([]byte("/theme mono\r\n/quiet\r\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
close(connSignal) // Connection is established so we can signal the success.
|
||||
return b.handleSSHChat()
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = sshd.ConnectShell(b.GetString("Server"), b.GetString("Nick"), func(r io.Reader, w io.WriteCloser) error {
|
||||
b.r = bufio.NewScanner(r)
|
||||
b.w = w
|
||||
b.r.Scan()
|
||||
w.Write([]byte("/theme mono\r\n"))
|
||||
b.handleSSHChat()
|
||||
return nil
|
||||
})
|
||||
// As a successful connection will result in this returning after the Connection
|
||||
// method has already returned point we NEED to have a buffered channel to still
|
||||
// be able to write.
|
||||
connErr <- sshd.ConnectShell(b.GetString("Server"), b.GetString("Nick"), connHandler)
|
||||
}()
|
||||
if err != nil {
|
||||
b.Log.Debugf("%#v", err)
|
||||
|
||||
select {
|
||||
case err := <-connErr:
|
||||
b.Log.Error("Connection failed")
|
||||
return err
|
||||
case <-connSignal:
|
||||
}
|
||||
b.Log.Info("Connection succeeded")
|
||||
return nil
|
||||
@@ -59,27 +72,16 @@ func (b *Bsshchat) Send(msg config.Message) (string, error) {
|
||||
b.Log.Debugf("=> Receiving %#v", msg)
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.w.Write([]byte(rmsg.Username + rmsg.Text + "\r\n"))
|
||||
if _, err := b.w.Write([]byte(rmsg.Username + rmsg.Text + "\r\n")); err != nil {
|
||||
b.Log.Errorf("Could not send extra message: %#v", err)
|
||||
}
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text = fi.URL
|
||||
if fi.Comment != "" {
|
||||
msg.Text = fi.Comment + ": " + fi.URL
|
||||
}
|
||||
}
|
||||
b.w.Write([]byte(msg.Username + msg.Text))
|
||||
}
|
||||
return "", nil
|
||||
return b.handleUploadFile(&msg)
|
||||
}
|
||||
}
|
||||
b.w.Write([]byte(msg.Username + msg.Text + "\r\n"))
|
||||
return "", nil
|
||||
_, err := b.w.Write([]byte(msg.Username + msg.Text + "\r\n"))
|
||||
return "", err
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -125,17 +127,39 @@ func (b *Bsshchat) handleSSHChat() error {
|
||||
if !strings.Contains(b.r.Text(), "\033[K") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(b.r.Text(), "Rate limiting is in effect") {
|
||||
continue
|
||||
}
|
||||
res := strings.Split(stripPrompt(b.r.Text()), ":")
|
||||
if res[0] == "-> Set theme" {
|
||||
wait = false
|
||||
log.Debugf("mono found, allowing")
|
||||
b.Log.Debugf("mono found, allowing")
|
||||
continue
|
||||
}
|
||||
if !wait {
|
||||
b.Log.Debugf("<= Message %#v", res)
|
||||
rmsg := config.Message{Username: res[0], Text: strings.Join(res[1:], ":"), Channel: "sshchat", Account: b.Account, UserID: "nick"}
|
||||
rmsg := config.Message{Username: res[0], Text: strings.TrimSpace(strings.Join(res[1:], ":")), Channel: "sshchat", Account: b.Account, UserID: "nick"}
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bsshchat) handleUploadFile(msg *config.Message) (string, error) {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text = fi.URL
|
||||
if fi.Comment != "" {
|
||||
msg.Text = fi.Comment + ": " + fi.URL
|
||||
}
|
||||
}
|
||||
if _, err := b.w.Write([]byte(msg.Username + msg.Text + "\r\n")); err != nil {
|
||||
b.Log.Errorf("Could not send file message: %#v", err)
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
126
bridge/steam/handlers.go
Normal file
126
bridge/steam/handlers.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package bsteam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/Philipp15b/go-steam"
|
||||
"github.com/Philipp15b/go-steam/protocol/steamlang"
|
||||
)
|
||||
|
||||
func (b *Bsteam) handleChatMsg(e *steam.ChatMsgEvent) {
|
||||
b.Log.Debugf("Receiving ChatMsgEvent: %#v", e)
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account)
|
||||
var channel int64
|
||||
if e.ChatRoomId == 0 {
|
||||
channel = int64(e.ChatterId)
|
||||
} else {
|
||||
// for some reason we have to remove 0x18000000000000
|
||||
// TODO
|
||||
// https://github.com/42wim/matterbridge/pull/630#discussion_r238102751
|
||||
// channel = int64(e.ChatRoomId) & 0xfffffffffffff
|
||||
channel = int64(e.ChatRoomId) - 0x18000000000000
|
||||
}
|
||||
msg := config.Message{
|
||||
Username: b.getNick(e.ChatterId),
|
||||
Text: e.Message,
|
||||
Channel: strconv.FormatInt(channel, 10),
|
||||
Account: b.Account,
|
||||
UserID: strconv.FormatInt(int64(e.ChatterId), 10),
|
||||
}
|
||||
b.Remote <- msg
|
||||
}
|
||||
|
||||
func (b *Bsteam) handleEvents() {
|
||||
myLoginInfo := &steam.LogOnDetails{
|
||||
Username: b.GetString("Login"),
|
||||
Password: b.GetString("Password"),
|
||||
AuthCode: b.GetString("AuthCode"),
|
||||
}
|
||||
// TODO Attempt to read existing auth hash to avoid steam guard.
|
||||
// Maybe works
|
||||
//myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry")
|
||||
for event := range b.c.Events() {
|
||||
switch e := event.(type) {
|
||||
case *steam.ChatMsgEvent:
|
||||
b.handleChatMsg(e)
|
||||
case *steam.PersonaStateEvent:
|
||||
b.Log.Debugf("PersonaStateEvent: %#v\n", e)
|
||||
b.Lock()
|
||||
b.userMap[e.FriendId] = e.Name
|
||||
b.Unlock()
|
||||
case *steam.ConnectedEvent:
|
||||
b.c.Auth.LogOn(myLoginInfo)
|
||||
case *steam.MachineAuthUpdateEvent:
|
||||
// TODO sentry files for 2 auth
|
||||
/*
|
||||
b.Log.Info("authupdate", e)
|
||||
b.Log.Info("hash", e.Hash)
|
||||
ioutil.WriteFile("sentry", e.Hash, 0666)
|
||||
*/
|
||||
case *steam.LogOnFailedEvent:
|
||||
b.Log.Info("Logon failed", e)
|
||||
err := b.handleLogOnFailed(e, myLoginInfo)
|
||||
if err != nil {
|
||||
b.Log.Error(err)
|
||||
return
|
||||
}
|
||||
case *steam.LoggedOnEvent:
|
||||
b.Log.Debugf("LoggedOnEvent: %#v", e)
|
||||
b.connected <- struct{}{}
|
||||
b.Log.Debugf("setting online")
|
||||
b.c.Social.SetPersonaState(steamlang.EPersonaState_Online)
|
||||
case *steam.DisconnectedEvent:
|
||||
b.Log.Info("Disconnected")
|
||||
b.Log.Info("Attempting to reconnect...")
|
||||
b.c.Connect()
|
||||
case steam.FatalErrorEvent:
|
||||
b.Log.Errorf("steam FatalErrorEvent: %#v", e)
|
||||
default:
|
||||
b.Log.Debugf("unknown event %#v", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bsteam) handleLogOnFailed(e *steam.LogOnFailedEvent, myLoginInfo *steam.LogOnDetails) error {
|
||||
switch e.Result {
|
||||
case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode:
|
||||
b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:")
|
||||
var code string
|
||||
fmt.Scanf("%s", &code)
|
||||
// TODO https://github.com/42wim/matterbridge/pull/630#discussion_r238103978
|
||||
myLoginInfo.TwoFactorCode = code
|
||||
case steamlang.EResult_AccountLogonDenied:
|
||||
b.Log.Info("Steam guard isn't letting me in! Enter auth code:")
|
||||
var code string
|
||||
fmt.Scanf("%s", &code)
|
||||
// TODO https://github.com/42wim/matterbridge/pull/630#discussion_r238103978
|
||||
myLoginInfo.AuthCode = code
|
||||
case steamlang.EResult_InvalidLoginAuthCode:
|
||||
return fmt.Errorf("Steam guard: invalid login auth code: %#v ", e.Result)
|
||||
default:
|
||||
return fmt.Errorf("LogOnFailedEvent: %#v ", e.Result)
|
||||
// TODO: Handle EResult_InvalidLoginAuthCode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleFileInfo handles config.FileInfo and adds correct file comment or URL to msg.Text.
|
||||
// Returns error if cast fails.
|
||||
func (b *Bsteam) handleFileInfo(msg *config.Message, f interface{}) error {
|
||||
if _, ok := f.(config.FileInfo); !ok {
|
||||
return fmt.Errorf("handleFileInfo cast failed %#v", f)
|
||||
}
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text = fi.URL
|
||||
if fi.Comment != "" {
|
||||
msg.Text = fi.Comment + ": " + fi.URL
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package bsteam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -73,22 +72,13 @@ func (b *Bsteam) Send(msg config.Message) (string, error) {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, rmsg.Username+rmsg.Text)
|
||||
}
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text = fi.URL
|
||||
if fi.Comment != "" {
|
||||
msg.Text = fi.Comment + ": " + fi.URL
|
||||
}
|
||||
}
|
||||
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
|
||||
for i := range msg.Extra["file"] {
|
||||
if err := b.handleFileInfo(&msg, msg.Extra["file"][i]); err != nil {
|
||||
b.Log.Error(err)
|
||||
}
|
||||
return "", nil
|
||||
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
|
||||
@@ -103,78 +93,3 @@ func (b *Bsteam) getNick(id steamid.SteamId) string {
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (b *Bsteam) handleEvents() {
|
||||
myLoginInfo := new(steam.LogOnDetails)
|
||||
myLoginInfo.Username = b.GetString("Login")
|
||||
myLoginInfo.Password = b.GetString("Password")
|
||||
myLoginInfo.AuthCode = b.GetString("AuthCode")
|
||||
// Attempt to read existing auth hash to avoid steam guard.
|
||||
// Maybe works
|
||||
//myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry")
|
||||
for event := range b.c.Events() {
|
||||
//b.Log.Info(event)
|
||||
switch e := event.(type) {
|
||||
case *steam.ChatMsgEvent:
|
||||
b.Log.Debugf("Receiving ChatMsgEvent: %#v", e)
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account)
|
||||
var channel int64
|
||||
if e.ChatRoomId == 0 {
|
||||
channel = int64(e.ChatterId)
|
||||
} else {
|
||||
// for some reason we have to remove 0x18000000000000
|
||||
channel = int64(e.ChatRoomId) - 0x18000000000000
|
||||
}
|
||||
msg := config.Message{Username: b.getNick(e.ChatterId), Text: e.Message, Channel: strconv.FormatInt(channel, 10), Account: b.Account, UserID: strconv.FormatInt(int64(e.ChatterId), 10)}
|
||||
b.Remote <- msg
|
||||
case *steam.PersonaStateEvent:
|
||||
b.Log.Debugf("PersonaStateEvent: %#v\n", e)
|
||||
b.Lock()
|
||||
b.userMap[e.FriendId] = e.Name
|
||||
b.Unlock()
|
||||
case *steam.ConnectedEvent:
|
||||
b.c.Auth.LogOn(myLoginInfo)
|
||||
case *steam.MachineAuthUpdateEvent:
|
||||
/*
|
||||
b.Log.Info("authupdate", e)
|
||||
b.Log.Info("hash", e.Hash)
|
||||
ioutil.WriteFile("sentry", e.Hash, 0666)
|
||||
*/
|
||||
case *steam.LogOnFailedEvent:
|
||||
b.Log.Info("Logon failed", e)
|
||||
switch e.Result {
|
||||
case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode:
|
||||
{
|
||||
b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:")
|
||||
var code string
|
||||
fmt.Scanf("%s", &code)
|
||||
myLoginInfo.TwoFactorCode = code
|
||||
}
|
||||
case steamlang.EResult_AccountLogonDenied:
|
||||
{
|
||||
b.Log.Info("Steam guard isn't letting me in! Enter auth code:")
|
||||
var code string
|
||||
fmt.Scanf("%s", &code)
|
||||
myLoginInfo.AuthCode = code
|
||||
}
|
||||
default:
|
||||
b.Log.Errorf("LogOnFailedEvent: %#v ", e.Result)
|
||||
// TODO: Handle EResult_InvalidLoginAuthCode
|
||||
return
|
||||
}
|
||||
case *steam.LoggedOnEvent:
|
||||
b.Log.Debugf("LoggedOnEvent: %#v", e)
|
||||
b.connected <- struct{}{}
|
||||
b.Log.Debugf("setting online")
|
||||
b.c.Social.SetPersonaState(steamlang.EPersonaState_Online)
|
||||
case *steam.DisconnectedEvent:
|
||||
b.Log.Info("Disconnected")
|
||||
b.Log.Info("Attempting to reconnect...")
|
||||
b.c.Connect()
|
||||
case steam.FatalErrorEvent:
|
||||
b.Log.Error(e)
|
||||
default:
|
||||
b.Log.Debugf("unknown event %#v", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
388
bridge/telegram/handlers.go
Normal file
388
bridge/telegram/handlers.go
Normal file
@@ -0,0 +1,388 @@
|
||||
package btelegram
|
||||
|
||||
import (
|
||||
"html"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
|
||||
// handle channels
|
||||
if posted != nil {
|
||||
message = posted
|
||||
rmsg.Text = message.Text
|
||||
}
|
||||
|
||||
// edited channel message
|
||||
if edited != nil && !b.GetBool("EditDisable") {
|
||||
message = edited
|
||||
rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix")
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// handleChannels checks if it's a channel message and if the message is a new or edited messages
|
||||
func (b *Btelegram) handleChannels(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
|
||||
return b.handleUpdate(rmsg, message, update.ChannelPost, update.EditedChannelPost)
|
||||
}
|
||||
|
||||
// handleGroups checks if it's a group message and if the message is a new or edited messages
|
||||
func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
|
||||
return b.handleUpdate(rmsg, message, update.Message, update.EditedMessage)
|
||||
}
|
||||
|
||||
// handleForwarded handles forwarded messages
|
||||
func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) {
|
||||
if message.ForwardFrom != nil {
|
||||
usernameForward := ""
|
||||
if b.GetBool("UseFirstName") {
|
||||
usernameForward = message.ForwardFrom.FirstName
|
||||
}
|
||||
if usernameForward == "" {
|
||||
usernameForward = message.ForwardFrom.UserName
|
||||
if usernameForward == "" {
|
||||
usernameForward = message.ForwardFrom.FirstName
|
||||
}
|
||||
}
|
||||
if usernameForward == "" {
|
||||
usernameForward = unknownUser
|
||||
}
|
||||
rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
|
||||
}
|
||||
}
|
||||
|
||||
// handleQuoting handles quoting of previous messages
|
||||
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
|
||||
if message.ReplyToMessage != nil {
|
||||
usernameReply := ""
|
||||
if message.ReplyToMessage.From != nil {
|
||||
if b.GetBool("UseFirstName") {
|
||||
usernameReply = message.ReplyToMessage.From.FirstName
|
||||
}
|
||||
if usernameReply == "" {
|
||||
usernameReply = message.ReplyToMessage.From.UserName
|
||||
if usernameReply == "" {
|
||||
usernameReply = message.ReplyToMessage.From.FirstName
|
||||
}
|
||||
}
|
||||
}
|
||||
if usernameReply == "" {
|
||||
usernameReply = unknownUser
|
||||
}
|
||||
if !b.GetBool("QuoteDisable") {
|
||||
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleUsername handles the correct setting of the username
|
||||
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
|
||||
if message.From != nil {
|
||||
rmsg.UserID = strconv.Itoa(message.From.ID)
|
||||
if b.GetBool("UseFirstName") {
|
||||
rmsg.Username = message.From.FirstName
|
||||
}
|
||||
if rmsg.Username == "" {
|
||||
rmsg.Username = message.From.UserName
|
||||
if rmsg.Username == "" {
|
||||
rmsg.Username = message.From.FirstName
|
||||
}
|
||||
}
|
||||
// only download avatars if we have a place to upload them (configured mediaserver)
|
||||
if b.General.MediaServerUpload != "" {
|
||||
b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
|
||||
}
|
||||
}
|
||||
|
||||
// if we really didn't find a username, set it to unknown
|
||||
if rmsg.Username == "" {
|
||||
rmsg.Username = unknownUser
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
||||
for update := range updates {
|
||||
b.Log.Debugf("== Receiving event: %#v", update.Message)
|
||||
|
||||
if update.Message == nil && update.ChannelPost == nil &&
|
||||
update.EditedMessage == nil && update.EditedChannelPost == nil {
|
||||
b.Log.Error("Getting nil messages, this shouldn't happen.")
|
||||
continue
|
||||
}
|
||||
|
||||
var message *tgbotapi.Message
|
||||
|
||||
rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
|
||||
|
||||
// handle channels
|
||||
message = b.handleChannels(&rmsg, message, update)
|
||||
|
||||
// handle groups
|
||||
message = b.handleGroups(&rmsg, message, update)
|
||||
|
||||
if message == nil {
|
||||
b.Log.Error("message is nil, this shouldn't happen.")
|
||||
continue
|
||||
}
|
||||
|
||||
// set the ID's from the channel or group message
|
||||
rmsg.ID = strconv.Itoa(message.MessageID)
|
||||
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
|
||||
|
||||
// handle username
|
||||
b.handleUsername(&rmsg, message)
|
||||
|
||||
// handle any downloads
|
||||
err := b.handleDownload(&rmsg, message)
|
||||
if err != nil {
|
||||
b.Log.Errorf("download failed: %s", err)
|
||||
}
|
||||
|
||||
// handle forwarded messages
|
||||
b.handleForwarded(&rmsg, message)
|
||||
|
||||
// quote the previous message
|
||||
b.handleQuoting(&rmsg, message)
|
||||
|
||||
// handle entities (adding URLs)
|
||||
b.handleEntities(&rmsg, message)
|
||||
|
||||
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
|
||||
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
|
||||
// channels don't have (always?) user information. see #410
|
||||
if message.From != nil {
|
||||
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
|
||||
}
|
||||
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleDownloadAvatar downloads the avatar of userid from channel
|
||||
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
|
||||
// logs an error message if it fails
|
||||
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
|
||||
rmsg := config.Message{Username: "system",
|
||||
Text: "avatar",
|
||||
Channel: channel,
|
||||
Account: b.Account,
|
||||
UserID: strconv.Itoa(userid),
|
||||
Event: config.EventAvatarDownload,
|
||||
Extra: make(map[string][]interface{})}
|
||||
|
||||
if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
|
||||
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
|
||||
if err != nil {
|
||||
b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
|
||||
}
|
||||
|
||||
if len(photos.Photos) > 0 {
|
||||
photo := photos.Photos[0][0]
|
||||
url := b.getFileDirectURL(photo.FileID)
|
||||
name := strconv.Itoa(userid) + ".png"
|
||||
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
|
||||
|
||||
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
|
||||
if err != nil {
|
||||
b.Log.Error(err)
|
||||
return
|
||||
}
|
||||
data, err := helper.DownloadFile(url)
|
||||
if err != nil {
|
||||
b.Log.Errorf("download %s failed %#v", url, err)
|
||||
return
|
||||
}
|
||||
helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleDownloadFile handles file download
|
||||
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
|
||||
size := 0
|
||||
var url, name, text string
|
||||
switch {
|
||||
case message.Sticker != nil:
|
||||
text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
|
||||
size = message.Sticker.FileSize
|
||||
case message.Voice != nil:
|
||||
text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
|
||||
size = message.Voice.FileSize
|
||||
case message.Video != nil:
|
||||
text, name, url = b.getDownloadInfo(message.Video.FileID, "", true)
|
||||
size = message.Video.FileSize
|
||||
case message.Audio != nil:
|
||||
text, name, url = b.getDownloadInfo(message.Audio.FileID, "", true)
|
||||
size = message.Audio.FileSize
|
||||
case message.Document != nil:
|
||||
_, _, url = b.getDownloadInfo(message.Document.FileID, "", false)
|
||||
size = message.Document.FileSize
|
||||
name = message.Document.FileName
|
||||
text = " " + message.Document.FileName + " : " + url
|
||||
case message.Photo != nil:
|
||||
photos := *message.Photo
|
||||
size = photos[len(photos)-1].FileSize
|
||||
text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
|
||||
}
|
||||
|
||||
// if name is empty we didn't match a thing to download
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
// use the URL instead of native upload
|
||||
if b.GetBool("UseInsecureURL") {
|
||||
b.Log.Debugf("Setting message text to :%s", text)
|
||||
rmsg.Text += text
|
||||
return nil
|
||||
}
|
||||
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
|
||||
err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := helper.DownloadFile(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasSuffix(name, ".webp") && b.GetBool("MediaConvertWebPToPNG") {
|
||||
b.Log.Debugf("WebP to PNG conversion enabled, converting %s", name)
|
||||
err := helper.ConvertWebPToPNG(data)
|
||||
if err != nil {
|
||||
b.Log.Errorf("conversion failed: %s", err)
|
||||
} else {
|
||||
name = strings.Replace(name, ".webp", ".png", 1)
|
||||
}
|
||||
}
|
||||
helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (string, string, string) {
|
||||
url := b.getFileDirectURL(id)
|
||||
name := ""
|
||||
if urlpart {
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
}
|
||||
if suffix != "" && !strings.HasSuffix(name, suffix) {
|
||||
name += suffix
|
||||
}
|
||||
text := " " + url
|
||||
return text, name, url
|
||||
}
|
||||
|
||||
// handleDelete handles message deleting
|
||||
func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, error) {
|
||||
if msg.ID == "" {
|
||||
return "", nil
|
||||
}
|
||||
msgid, err := strconv.Atoi(msg.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
|
||||
return "", err
|
||||
}
|
||||
|
||||
// handleEdit handles message editing.
|
||||
func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error) {
|
||||
msgid, err := strconv.Atoi(msg.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
|
||||
b.Log.Debug("Using mode HTML - nick only")
|
||||
msg.Text = html.EscapeString(msg.Text)
|
||||
}
|
||||
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
|
||||
switch b.GetString("MessageFormat") {
|
||||
case HTMLFormat:
|
||||
b.Log.Debug("Using mode HTML")
|
||||
m.ParseMode = tgbotapi.ModeHTML
|
||||
case "Markdown":
|
||||
b.Log.Debug("Using mode markdown")
|
||||
m.ParseMode = tgbotapi.ModeMarkdown
|
||||
}
|
||||
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
|
||||
b.Log.Debug("Using mode HTML - nick only")
|
||||
m.ParseMode = tgbotapi.ModeHTML
|
||||
}
|
||||
_, err = b.c.Send(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// handleUploadFile handles native upload of files
|
||||
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
|
||||
var c tgbotapi.Chattable
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
file := tgbotapi.FileBytes{
|
||||
Name: fi.Name,
|
||||
Bytes: *fi.Data,
|
||||
}
|
||||
re := regexp.MustCompile(".(jpg|png)$")
|
||||
if re.MatchString(fi.Name) {
|
||||
c = tgbotapi.NewPhotoUpload(chatid, file)
|
||||
} else {
|
||||
c = tgbotapi.NewDocumentUpload(chatid, file)
|
||||
}
|
||||
_, err := b.c.Send(c)
|
||||
if err != nil {
|
||||
b.Log.Errorf("file upload failed: %#v", err)
|
||||
}
|
||||
if fi.Comment != "" {
|
||||
if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil {
|
||||
b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
|
||||
format := b.GetString("quoteformat")
|
||||
if format == "" {
|
||||
format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
|
||||
}
|
||||
format = strings.Replace(format, "{MESSAGE}", message, -1)
|
||||
format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1)
|
||||
format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)
|
||||
return format
|
||||
}
|
||||
|
||||
// handleEntities handles messageEntities
|
||||
func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Message) {
|
||||
if message.Entities == nil {
|
||||
return
|
||||
}
|
||||
// for now only do URL replacements
|
||||
for _, e := range *message.Entities {
|
||||
if e.Type == "text_link" {
|
||||
url, err := e.ParseURL()
|
||||
if err != nil {
|
||||
b.Log.Errorf("entity text_link url parse failed: %s", err)
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package btelegram
|
||||
import (
|
||||
"bytes"
|
||||
"html"
|
||||
"io"
|
||||
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
@@ -33,8 +32,8 @@ func (options *customHTML) Header(out *bytes.Buffer, text func() bool, level int
|
||||
options.Paragraph(out, text)
|
||||
}
|
||||
|
||||
func (options *customHTML) HRule(out io.ByteWriter) {
|
||||
out.WriteByte('\n')
|
||||
func (options *customHTML) HRule(out *bytes.Buffer) {
|
||||
out.WriteByte('\n') //nolint:errcheck
|
||||
}
|
||||
|
||||
func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) {
|
||||
@@ -54,16 +53,13 @@ func (options *customHTML) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||
}
|
||||
|
||||
func makeHTML(input string) string {
|
||||
extensions := blackfriday.NoIntraEmphasis |
|
||||
blackfriday.FencedCode |
|
||||
blackfriday.Autolink |
|
||||
blackfriday.SpaceHeadings |
|
||||
blackfriday.HeadingIDs |
|
||||
blackfriday.BackslashLineBreak |
|
||||
blackfriday.DefinitionLists
|
||||
|
||||
renderer := &customHTML{blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
|
||||
Flags: blackfriday.UseXHTML | blackfriday.SkipImages,
|
||||
})}
|
||||
return string(blackfriday.Run([]byte(input), blackfriday.WithExtensions(extensions), blackfriday.WithRenderer(renderer)))
|
||||
return string(blackfriday.Markdown([]byte(input),
|
||||
&customHTML{blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML|blackfriday.HTML_SKIP_IMAGES, "", "")},
|
||||
blackfriday.EXTENSION_NO_INTRA_EMPHASIS|
|
||||
blackfriday.EXTENSION_FENCED_CODE|
|
||||
blackfriday.EXTENSION_AUTOLINK|
|
||||
blackfriday.EXTENSION_SPACE_HEADERS|
|
||||
blackfriday.EXTENSION_HEADER_IDS|
|
||||
blackfriday.EXTENSION_BACKSLASH_LINE_BREAK|
|
||||
blackfriday.EXTENSION_DEFINITION_LISTS))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package btelegram
|
||||
|
||||
import (
|
||||
"html"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -76,21 +75,15 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
|
||||
|
||||
// Delete message
|
||||
if msg.Event == config.EventMsgDelete {
|
||||
if msg.ID == "" {
|
||||
return "", nil
|
||||
}
|
||||
msgid, err := strconv.Atoi(msg.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
|
||||
return "", err
|
||||
return b.handleDelete(&msg, chatid)
|
||||
}
|
||||
|
||||
// Upload a file if it exists
|
||||
if msg.Extra != nil {
|
||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
b.sendMessage(chatid, rmsg.Username, rmsg.Text)
|
||||
if _, err := b.sendMessage(chatid, rmsg.Username, rmsg.Text); err != nil {
|
||||
b.Log.Errorf("sendMessage failed: %s", err)
|
||||
}
|
||||
}
|
||||
// check if we have files to upload (from slack, telegram or mattermost)
|
||||
if len(msg.Extra["file"]) > 0 {
|
||||
@@ -100,162 +93,13 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
|
||||
|
||||
// edit the message if we have a msg ID
|
||||
if msg.ID != "" {
|
||||
msgid, err := strconv.Atoi(msg.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
|
||||
b.Log.Debug("Using mode HTML - nick only")
|
||||
msg.Text = html.EscapeString(msg.Text)
|
||||
}
|
||||
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
|
||||
if b.GetString("MessageFormat") == HTMLFormat {
|
||||
b.Log.Debug("Using mode HTML")
|
||||
m.ParseMode = tgbotapi.ModeHTML
|
||||
}
|
||||
if b.GetString("MessageFormat") == "Markdown" {
|
||||
b.Log.Debug("Using mode markdown")
|
||||
m.ParseMode = tgbotapi.ModeMarkdown
|
||||
}
|
||||
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
|
||||
b.Log.Debug("Using mode HTML - nick only")
|
||||
m.ParseMode = tgbotapi.ModeHTML
|
||||
}
|
||||
_, err = b.c.Send(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", nil
|
||||
return b.handleEdit(&msg, chatid)
|
||||
}
|
||||
|
||||
// Post normal message
|
||||
return b.sendMessage(chatid, msg.Username, msg.Text)
|
||||
}
|
||||
|
||||
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
||||
for update := range updates {
|
||||
b.Log.Debugf("== Receiving event: %#v", update.Message)
|
||||
|
||||
if update.Message == nil && update.ChannelPost == nil && update.EditedMessage == nil && update.EditedChannelPost == nil {
|
||||
b.Log.Error("Getting nil messages, this shouldn't happen.")
|
||||
continue
|
||||
}
|
||||
|
||||
var message *tgbotapi.Message
|
||||
|
||||
rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
|
||||
|
||||
// handle channels
|
||||
if update.ChannelPost != nil {
|
||||
message = update.ChannelPost
|
||||
rmsg.Text = message.Text
|
||||
}
|
||||
|
||||
// edited channel message
|
||||
if update.EditedChannelPost != nil && !b.GetBool("EditDisable") {
|
||||
message = update.EditedChannelPost
|
||||
rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix")
|
||||
}
|
||||
|
||||
// handle groups
|
||||
if update.Message != nil {
|
||||
message = update.Message
|
||||
rmsg.Text = message.Text
|
||||
}
|
||||
|
||||
// edited group message
|
||||
if update.EditedMessage != nil && !b.GetBool("EditDisable") {
|
||||
message = update.EditedMessage
|
||||
rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix")
|
||||
}
|
||||
|
||||
// set the ID's from the channel or group message
|
||||
rmsg.ID = strconv.Itoa(message.MessageID)
|
||||
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
|
||||
|
||||
// handle username
|
||||
if message.From != nil {
|
||||
rmsg.UserID = strconv.Itoa(message.From.ID)
|
||||
if b.GetBool("UseFirstName") {
|
||||
rmsg.Username = message.From.FirstName
|
||||
}
|
||||
if rmsg.Username == "" {
|
||||
rmsg.Username = message.From.UserName
|
||||
if rmsg.Username == "" {
|
||||
rmsg.Username = message.From.FirstName
|
||||
}
|
||||
}
|
||||
// only download avatars if we have a place to upload them (configured mediaserver)
|
||||
if b.General.MediaServerUpload != "" {
|
||||
b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
|
||||
}
|
||||
}
|
||||
|
||||
// if we really didn't find a username, set it to unknown
|
||||
if rmsg.Username == "" {
|
||||
rmsg.Username = unknownUser
|
||||
}
|
||||
|
||||
// handle any downloads
|
||||
err := b.handleDownload(message, &rmsg)
|
||||
if err != nil {
|
||||
b.Log.Errorf("download failed: %s", err)
|
||||
}
|
||||
|
||||
// handle forwarded messages
|
||||
if message.ForwardFrom != nil {
|
||||
usernameForward := ""
|
||||
if b.GetBool("UseFirstName") {
|
||||
usernameForward = message.ForwardFrom.FirstName
|
||||
}
|
||||
if usernameForward == "" {
|
||||
usernameForward = message.ForwardFrom.UserName
|
||||
if usernameForward == "" {
|
||||
usernameForward = message.ForwardFrom.FirstName
|
||||
}
|
||||
}
|
||||
if usernameForward == "" {
|
||||
usernameForward = unknownUser
|
||||
}
|
||||
rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
|
||||
}
|
||||
|
||||
// quote the previous message
|
||||
if message.ReplyToMessage != nil {
|
||||
usernameReply := ""
|
||||
if message.ReplyToMessage.From != nil {
|
||||
if b.GetBool("UseFirstName") {
|
||||
usernameReply = message.ReplyToMessage.From.FirstName
|
||||
}
|
||||
if usernameReply == "" {
|
||||
usernameReply = message.ReplyToMessage.From.UserName
|
||||
if usernameReply == "" {
|
||||
usernameReply = message.ReplyToMessage.From.FirstName
|
||||
}
|
||||
}
|
||||
}
|
||||
if usernameReply == "" {
|
||||
usernameReply = unknownUser
|
||||
}
|
||||
if !b.GetBool("QuoteDisable") {
|
||||
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
|
||||
}
|
||||
}
|
||||
|
||||
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
|
||||
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
|
||||
// channels don't have (always?) user information. see #410
|
||||
if message.From != nil {
|
||||
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
|
||||
}
|
||||
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Btelegram) getFileDirectURL(id string) string {
|
||||
res, err := b.c.GetFileDirectURL(id)
|
||||
if err != nil {
|
||||
@@ -264,146 +108,6 @@ func (b *Btelegram) getFileDirectURL(id string) string {
|
||||
return res
|
||||
}
|
||||
|
||||
// handleDownloadAvatar downloads the avatar of userid from channel
|
||||
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
|
||||
// logs an error message if it fails
|
||||
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
|
||||
rmsg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: strconv.Itoa(userid), Event: config.EventAvatarDownload, Extra: make(map[string][]interface{})}
|
||||
if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
|
||||
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
|
||||
if err != nil {
|
||||
b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
|
||||
}
|
||||
|
||||
if len(photos.Photos) > 0 {
|
||||
photo := photos.Photos[0][0]
|
||||
url := b.getFileDirectURL(photo.FileID)
|
||||
name := strconv.Itoa(userid) + ".png"
|
||||
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
|
||||
|
||||
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
|
||||
if err != nil {
|
||||
b.Log.Error(err)
|
||||
return
|
||||
}
|
||||
data, err := helper.DownloadFile(url)
|
||||
if err != nil {
|
||||
b.Log.Errorf("download %s failed %#v", url, err)
|
||||
return
|
||||
}
|
||||
helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleDownloadFile handles file download
|
||||
func (b *Btelegram) handleDownload(message *tgbotapi.Message, rmsg *config.Message) error {
|
||||
size := 0
|
||||
var url, name, text string
|
||||
|
||||
if message.Sticker != nil {
|
||||
v := message.Sticker
|
||||
size = v.FileSize
|
||||
url = b.getFileDirectURL(v.FileID)
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
if !strings.HasSuffix(name, ".webp") {
|
||||
name += ".webp"
|
||||
}
|
||||
text = " " + url
|
||||
}
|
||||
if message.Video != nil {
|
||||
v := message.Video
|
||||
size = v.FileSize
|
||||
url = b.getFileDirectURL(v.FileID)
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
text = " " + url
|
||||
}
|
||||
if message.Photo != nil {
|
||||
photos := *message.Photo
|
||||
size = photos[len(photos)-1].FileSize
|
||||
url = b.getFileDirectURL(photos[len(photos)-1].FileID)
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
text = " " + url
|
||||
}
|
||||
if message.Document != nil {
|
||||
v := message.Document
|
||||
size = v.FileSize
|
||||
url = b.getFileDirectURL(v.FileID)
|
||||
name = v.FileName
|
||||
text = " " + v.FileName + " : " + url
|
||||
}
|
||||
if message.Voice != nil {
|
||||
v := message.Voice
|
||||
size = v.FileSize
|
||||
url = b.getFileDirectURL(v.FileID)
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
text = " " + url
|
||||
if !strings.HasSuffix(name, ".ogg") {
|
||||
name += ".ogg"
|
||||
}
|
||||
}
|
||||
if message.Audio != nil {
|
||||
v := message.Audio
|
||||
size = v.FileSize
|
||||
url = b.getFileDirectURL(v.FileID)
|
||||
urlPart := strings.Split(url, "/")
|
||||
name = urlPart[len(urlPart)-1]
|
||||
text = " " + url
|
||||
}
|
||||
// if name is empty we didn't match a thing to download
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
// use the URL instead of native upload
|
||||
if b.GetBool("UseInsecureURL") {
|
||||
b.Log.Debugf("Setting message text to :%s", text)
|
||||
rmsg.Text += text
|
||||
return nil
|
||||
}
|
||||
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
|
||||
err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := helper.DownloadFile(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleUploadFile handles native upload of files
|
||||
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) (string, error) {
|
||||
var c tgbotapi.Chattable
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
file := tgbotapi.FileBytes{
|
||||
Name: fi.Name,
|
||||
Bytes: *fi.Data,
|
||||
}
|
||||
re := regexp.MustCompile(".(jpg|png)$")
|
||||
if re.MatchString(fi.Name) {
|
||||
c = tgbotapi.NewPhotoUpload(chatid, file)
|
||||
} else {
|
||||
c = tgbotapi.NewDocumentUpload(chatid, file)
|
||||
}
|
||||
_, err := b.c.Send(c)
|
||||
if err != nil {
|
||||
b.Log.Errorf("file upload failed: %#v", err)
|
||||
}
|
||||
if fi.Comment != "" {
|
||||
b.sendMessage(chatid, msg.Username, fi.Comment)
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) {
|
||||
m := tgbotapi.NewMessage(chatid, "")
|
||||
m.Text = username + text
|
||||
@@ -437,14 +141,3 @@ func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) {
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
|
||||
format := b.GetString("quoteformat")
|
||||
if format == "" {
|
||||
format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
|
||||
}
|
||||
format = strings.Replace(format, "{MESSAGE}", message, -1)
|
||||
format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1)
|
||||
format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)
|
||||
return format
|
||||
}
|
||||
|
||||
105
bridge/whatsapp/handlers.go
Normal file
105
bridge/whatsapp/handlers.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package bwhatsapp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
)
|
||||
|
||||
/*
|
||||
Implement handling messages coming from WhatsApp
|
||||
Check:
|
||||
- https://github.com/Rhymen/go-whatsapp#add-message-handlers
|
||||
- https://github.com/Rhymen/go-whatsapp/blob/master/handler.go
|
||||
- https://github.com/tulir/mautrix-whatsapp/tree/master/whatsapp-ext for more advanced command handling
|
||||
*/
|
||||
|
||||
// HandleError received from WhatsApp
|
||||
func (b *Bwhatsapp) HandleError(err error) {
|
||||
// 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
|
||||
}
|
||||
|
||||
// HandleTextMessage sent from WhatsApp, relay it to the brige
|
||||
func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
|
||||
if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") {
|
||||
return
|
||||
}
|
||||
// whatsapp sends last messages to show context , cut them
|
||||
if message.Info.Timestamp < b.startedAt {
|
||||
return
|
||||
}
|
||||
|
||||
messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
|
||||
groupJid := message.Info.RemoteJid
|
||||
|
||||
senderJid := message.Info.SenderJid
|
||||
if len(senderJid) == 0 {
|
||||
// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
|
||||
senderJid = *message.Info.Source.Participant
|
||||
}
|
||||
|
||||
// translate sender's Jid to the nicest username we can get
|
||||
senderName := b.getSenderName(senderJid)
|
||||
if senderName == "" {
|
||||
senderName = "Someone" // don't expose telephone number
|
||||
}
|
||||
|
||||
extText := message.Info.Source.Message.ExtendedTextMessage
|
||||
if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil {
|
||||
// handle user mentions
|
||||
for _, mentionedJid := range extText.ContextInfo.MentionedJid {
|
||||
numberAndSuffix := strings.SplitN(mentionedJid, "@", 2)
|
||||
|
||||
// mentions comes as telephone numbers and we don't want to expose it to other bridges
|
||||
// replace it with something more meaninful to others
|
||||
mention := b.getSenderNotify(numberAndSuffix[0] + "@s.whatsapp.net")
|
||||
if mention == "" {
|
||||
mention = "someone"
|
||||
}
|
||||
message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
|
||||
}
|
||||
}
|
||||
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJid, b.Account)
|
||||
rmsg := config.Message{
|
||||
UserID: senderJid,
|
||||
Username: senderName,
|
||||
Text: message.Text,
|
||||
Timestamp: messageTime,
|
||||
Channel: groupJid,
|
||||
Account: b.Account,
|
||||
Protocol: b.Protocol,
|
||||
Extra: make(map[string][]interface{}),
|
||||
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
|
||||
// Event string `json:"event"`
|
||||
// Gateway string // will be added during message processing
|
||||
ID: message.Info.Id}
|
||||
|
||||
if avatarURL, exists := b.userAvatars[senderJid]; exists {
|
||||
rmsg.Avatar = avatarURL
|
||||
}
|
||||
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
//
|
||||
//func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
|
||||
// fmt.Println(message) // TODO implement
|
||||
//}
|
||||
//
|
||||
//func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
|
||||
// fmt.Println(message) // TODO implement
|
||||
//}
|
||||
//
|
||||
//func (b *Bwhatsapp) HandleJsonMessage(message string) {
|
||||
// fmt.Println(message) // TODO implement
|
||||
//}
|
||||
// TODO HandleRawMessage
|
||||
// TODO HandleAudioMessage
|
||||
107
bridge/whatsapp/helpers.go
Normal file
107
bridge/whatsapp/helpers.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package bwhatsapp
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
|
||||
"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 {
|
||||
qr := make(chan string)
|
||||
go func() {
|
||||
terminal := qrcodeTerminal.New()
|
||||
if invert {
|
||||
terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium)
|
||||
}
|
||||
|
||||
terminal.Get(<-qr).Print()
|
||||
}()
|
||||
|
||||
return qr
|
||||
}
|
||||
|
||||
func (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
|
||||
session := whatsapp.Session{}
|
||||
sessionFile := b.Config.GetString(sessionFile)
|
||||
|
||||
if sessionFile == "" {
|
||||
return session, errors.New("if you won't set SessionFile then you will need to scan QR code on every restart")
|
||||
}
|
||||
|
||||
file, err := os.Open(sessionFile)
|
||||
if err != nil {
|
||||
return session, err
|
||||
}
|
||||
defer file.Close()
|
||||
decoder := gob.NewDecoder(file)
|
||||
err = decoder.Decode(&session)
|
||||
if err != nil {
|
||||
return session, err
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
|
||||
sessionFile := b.Config.GetString(sessionFile)
|
||||
|
||||
if sessionFile == "" {
|
||||
// we already sent a warning while starting the bridge, so let's be quiet here
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Create(sessionFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
encoder := gob.NewEncoder(file)
|
||||
err = encoder.Encode(session)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Bwhatsapp) getSenderName(senderJid string) string {
|
||||
if sender, exists := b.users[senderJid]; exists {
|
||||
if sender.Name != "" {
|
||||
return sender.Name
|
||||
}
|
||||
// if user is not in phone contacts
|
||||
// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
|
||||
// users can change it in their WhatsApp settings -> profile -> click on Avatar
|
||||
return sender.Notify
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
|
||||
if sender, exists := b.users[senderJid]; exists {
|
||||
return sender.Notify
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
298
bridge/whatsapp/whatsapp.go
Normal file
298
bridge/whatsapp/whatsapp.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package bwhatsapp
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
)
|
||||
|
||||
const (
|
||||
// Account config parameters
|
||||
cfgNumber = "Number"
|
||||
qrOnWhiteTerminal = "QrOnWhiteTerminal"
|
||||
sessionFile = "SessionFile"
|
||||
)
|
||||
|
||||
// Bwhatsapp Bridge structure keeping all the information needed for relying
|
||||
type Bwhatsapp struct {
|
||||
*bridge.Config
|
||||
|
||||
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21
|
||||
session *whatsapp.Session
|
||||
conn *whatsapp.Conn
|
||||
startedAt uint64
|
||||
|
||||
users map[string]whatsapp.Contact
|
||||
userAvatars map[string]string
|
||||
}
|
||||
|
||||
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
|
||||
func New(cfg *bridge.Config) bridge.Bridger {
|
||||
number := cfg.GetString(cfgNumber)
|
||||
if number == "" {
|
||||
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
|
||||
}
|
||||
|
||||
b := &Bwhatsapp{
|
||||
Config: cfg,
|
||||
|
||||
users: make(map[string]whatsapp.Contact),
|
||||
userAvatars: make(map[string]string),
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Connect to WhatsApp. Required implementation of the Bridger interface
|
||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
|
||||
func (b *Bwhatsapp) Connect() error {
|
||||
b.RLock() // TODO do we need locking for Whatsapp?
|
||||
defer b.RUnlock()
|
||||
|
||||
number := b.GetString(cfgNumber)
|
||||
if number == "" {
|
||||
return errors.New("WhatsApp's telephone Number need to be configured")
|
||||
}
|
||||
|
||||
// https://github.com/Rhymen/go-whatsapp#creating-a-connection
|
||||
b.Log.Debugln("Connecting to WhatsApp..")
|
||||
conn, err := whatsapp.NewConn(20 * time.Second)
|
||||
if err != nil {
|
||||
return errors.New("failed to connect to WhatsApp: " + err.Error())
|
||||
}
|
||||
|
||||
b.conn = conn
|
||||
|
||||
b.conn.AddHandler(b)
|
||||
b.Log.Debugln("WhatsApp connection successful")
|
||||
|
||||
// load existing session in order to keep it between restarts
|
||||
if b.session == nil {
|
||||
var session whatsapp.Session
|
||||
session, err = b.readSession()
|
||||
|
||||
if err == nil {
|
||||
b.Log.Debugln("Restoring WhatsApp session..")
|
||||
|
||||
// https://github.com/Rhymen/go-whatsapp#restore
|
||||
session, err = b.conn.RestoreWithSession(session)
|
||||
if err != nil {
|
||||
// TODO return or continue to normal login?
|
||||
// restore session connection timed out (I couldn't get over it without logging in again)
|
||||
return errors.New("failed to restore session: " + err.Error())
|
||||
}
|
||||
|
||||
b.session = &session
|
||||
b.Log.Debugln("Session restored successfully!")
|
||||
} else {
|
||||
b.Log.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// login to a new session
|
||||
if b.session == nil {
|
||||
err = b.Login()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
b.startedAt = uint64(time.Now().Unix())
|
||||
|
||||
_, err = b.conn.Contacts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on update of contacts: %v", err)
|
||||
}
|
||||
|
||||
// map all the users
|
||||
for id, contact := range b.conn.Store.Contacts {
|
||||
if !isGroupJid(id) && id != "status@broadcast" {
|
||||
// it is user
|
||||
b.users[id] = contact
|
||||
}
|
||||
}
|
||||
|
||||
// get user avatar asynchronously
|
||||
go func() {
|
||||
b.Log.Debug("Getting user avatars..")
|
||||
|
||||
for jid := range b.users {
|
||||
info, err := b.GetProfilePicThumb(jid)
|
||||
if err != nil {
|
||||
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
|
||||
|
||||
} else {
|
||||
// TODO any race conditions here?
|
||||
b.userAvatars[jid] = info.URL
|
||||
}
|
||||
}
|
||||
b.Log.Debug("Finished getting avatars..")
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Login to WhatsApp creating a new session. This will require to scan a QR code on your mobile device
|
||||
func (b *Bwhatsapp) Login() error {
|
||||
b.Log.Debugln("Logging in..")
|
||||
|
||||
invert := b.GetBool(qrOnWhiteTerminal) // false is the default
|
||||
qrChan := qrFromTerminal(invert)
|
||||
|
||||
session, err := b.conn.Login(qrChan)
|
||||
if err != nil {
|
||||
b.Log.Warnln("Failed to log in:", err)
|
||||
return err
|
||||
}
|
||||
b.session = &session
|
||||
|
||||
b.Log.Infof("Logged into session: %#v", session)
|
||||
b.Log.Infof("Connection: %#v", b.conn)
|
||||
|
||||
err = b.writeSession(session)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
|
||||
}
|
||||
|
||||
// TODO change connection strings to configured ones longClientName:"github.com/rhymen/go-whatsapp", shortClientName:"go-whatsapp"}" prefix=whatsapp
|
||||
// TODO get also a nice logo
|
||||
|
||||
// TODO notification about unplugged and dead battery
|
||||
// conn.Info: Wid, Pushname, Connected, Battery, Plugged
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect is called while reconnecting to the bridge
|
||||
// TODO 42wim Documentation would be helpful on when reconnects happen and what should be done in this function
|
||||
// Required implementation of the Bridger interface
|
||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
|
||||
func (b *Bwhatsapp) Disconnect() error {
|
||||
// We could Logout, but that would close the session completely and would require a new QR code scan
|
||||
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
|
||||
return nil
|
||||
}
|
||||
|
||||
func isGroupJid(identifier string) bool {
|
||||
return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp")
|
||||
}
|
||||
|
||||
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
|
||||
// Required implementation of the Bridger interface
|
||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
|
||||
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
|
||||
byJid := isGroupJid(channel.Name)
|
||||
|
||||
// verify if we are member of the given group
|
||||
if byJid {
|
||||
// channel.Name specifies static group jID, not the name
|
||||
if _, exists := b.conn.Store.Contacts[channel.Name]; !exists {
|
||||
return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name)
|
||||
}
|
||||
} else {
|
||||
// channel.Name specifies group name that might change, warn about it
|
||||
var jids []string
|
||||
for id, contact := range b.conn.Store.Contacts {
|
||||
if isGroupJid(id) && contact.Name == channel.Name {
|
||||
jids = append(jids, id)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(jids) {
|
||||
case 0:
|
||||
// didn't match any group - print out possibilites
|
||||
// TODO sort
|
||||
// copy b;
|
||||
//sort.Slice(people, func(i, j int) bool {
|
||||
// return people[i].Age > people[j].Age
|
||||
//})
|
||||
for id, contact := range b.conn.Store.Contacts {
|
||||
if isGroupJid(id) {
|
||||
b.Log.Infof("%s %s", contact.Jid, contact.Name)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
|
||||
|
||||
case 1:
|
||||
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send a message from the bridge to WhatsApp
|
||||
// Required implementation of the Bridger interface
|
||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
|
||||
func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
|
||||
b.Log.Debugf("=> Receiving %#v", msg)
|
||||
|
||||
// Delete message
|
||||
if msg.Event == config.EventMsgDelete {
|
||||
if msg.ID == "" {
|
||||
// No message ID in case action is executed on a message sent before the bridge was started
|
||||
// and then the bridge cache doesn't have this message ID mapped
|
||||
|
||||
// TODO 42wim Doesn't the app get clogged with a ton of IDs after some time of running?
|
||||
// WhatsApp allows to set any ID so in that case we could use external IDs and don't do mapping
|
||||
// but external IDs are not set
|
||||
return "", nil
|
||||
}
|
||||
// TODO delete message on WhatsApp https://github.com/Rhymen/go-whatsapp/issues/100
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Edit message
|
||||
if msg.ID != "" {
|
||||
b.Log.Debugf("updating message with id %s", msg.ID)
|
||||
|
||||
msg.Text += " (edited)"
|
||||
// TODO handle edit as a message reply with updated text
|
||||
}
|
||||
|
||||
//// TODO Handle Upload a file
|
||||
//if msg.Extra != nil {
|
||||
// for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||
// b.c.SendMessage(roomID, rmsg.Username+rmsg.Text)
|
||||
// }
|
||||
// if len(msg.Extra["file"]) > 0 {
|
||||
// return b.handleUploadFile(&msg, roomID)
|
||||
// }
|
||||
//}
|
||||
|
||||
// Post text message
|
||||
text := whatsapp.TextMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: msg.Channel, // which equals to group id
|
||||
},
|
||||
Text: msg.Username + msg.Text,
|
||||
}
|
||||
|
||||
b.Log.Debugf("=> Sending %#v", msg)
|
||||
|
||||
// create message ID
|
||||
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
|
||||
bytes := make([]byte, 10)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
b.Log.Warn(err.Error())
|
||||
}
|
||||
text.Info.Id = strings.ToUpper(hex.EncodeToString(bytes))
|
||||
|
||||
_, err := b.conn.Send(text)
|
||||
|
||||
return text.Info.Id, err
|
||||
}
|
||||
|
||||
// TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76
|
||||
//func (b *Bwhatsapp) Command(cmd string) string {
|
||||
// return ""
|
||||
//}
|
||||
@@ -2,7 +2,9 @@ package bxmpp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
@@ -14,49 +16,31 @@ import (
|
||||
)
|
||||
|
||||
type Bxmpp struct {
|
||||
xc *xmpp.Client
|
||||
xmppMap map[string]string
|
||||
*bridge.Config
|
||||
|
||||
startTime time.Time
|
||||
xc *xmpp.Client
|
||||
xmppMap map[string]string
|
||||
connected bool
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func New(cfg *bridge.Config) bridge.Bridger {
|
||||
b := &Bxmpp{Config: cfg}
|
||||
b.xmppMap = make(map[string]string)
|
||||
return b
|
||||
return &Bxmpp{
|
||||
Config: cfg,
|
||||
xmppMap: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bxmpp) Connect() error {
|
||||
var err error
|
||||
b.Log.Infof("Connecting %s", b.GetString("Server"))
|
||||
b.xc, err = b.createXMPP()
|
||||
if err != nil {
|
||||
if err := b.createXMPP(); err != nil {
|
||||
b.Log.Debugf("%#v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
b.Log.Info("Connection succeeded")
|
||||
go func() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}()
|
||||
go b.manageConnection()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -75,40 +59,58 @@ func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) 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
|
||||
if msg.Event == config.EventMsgDelete {
|
||||
return "", nil
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
return b.handleUploadFile(&msg)
|
||||
return "", b.handleUploadFile(&msg)
|
||||
}
|
||||
}
|
||||
|
||||
var msgreplaceid string
|
||||
msgid := xid.New().String()
|
||||
var msgReplaceID string
|
||||
msgID := xid.New().String()
|
||||
if msg.ID != "" {
|
||||
msgid = msg.ID
|
||||
msgreplaceid = msg.ID
|
||||
msgID = msg.ID
|
||||
msgReplaceID = msg.ID
|
||||
}
|
||||
// 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})
|
||||
if err != nil {
|
||||
// Post normal message.
|
||||
b.Log.Debugf("=> Sending message %#v", msg)
|
||||
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 msgid, nil
|
||||
return msgID, nil
|
||||
}
|
||||
|
||||
func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
|
||||
tc := new(tls.Config)
|
||||
tc.InsecureSkipVerify = b.GetBool("SkipTLSVerify")
|
||||
tc.ServerName = strings.Split(b.GetString("Server"), ":")[0]
|
||||
func (b *Bxmpp) createXMPP() error {
|
||||
tc := &tls.Config{
|
||||
ServerName: strings.Split(b.GetString("Jid"), "@")[1],
|
||||
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec
|
||||
}
|
||||
options := xmpp.Options{
|
||||
Host: b.GetString("Server"),
|
||||
User: b.GetString("Jid"),
|
||||
@@ -126,7 +128,54 @@ func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
|
||||
}
|
||||
var err error
|
||||
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 {
|
||||
@@ -138,8 +187,7 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
b.Log.Debugf("PING")
|
||||
err := b.xc.PingC2S("", "")
|
||||
if err != nil {
|
||||
if err := b.xc.PingC2S("", ""); err != nil {
|
||||
b.Log.Debugf("PING failed %#v", err)
|
||||
}
|
||||
case <-done:
|
||||
@@ -151,40 +199,59 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
|
||||
}
|
||||
|
||||
func (b *Bxmpp) handleXMPP() error {
|
||||
var ok bool
|
||||
var msgid string
|
||||
b.startTime = time.Now()
|
||||
|
||||
done := b.xmppKeepAlive()
|
||||
defer close(done)
|
||||
|
||||
for {
|
||||
m, err := b.xc.Recv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v := m.(type) {
|
||||
case xmpp.Chat:
|
||||
if v.Type == "groupchat" {
|
||||
b.Log.Debugf("== Receiving %#v", v)
|
||||
// skip invalid messages
|
||||
|
||||
// Skip invalid messages.
|
||||
if b.skipMessage(v) {
|
||||
continue
|
||||
}
|
||||
msgid = v.ID
|
||||
if v.ReplaceID != "" {
|
||||
msgid = v.ReplaceID
|
||||
}
|
||||
rmsg := config.Message{Username: b.parseNick(v.Remote), Text: v.Text, Channel: b.parseChannel(v.Remote), Account: b.Account, UserID: v.Remote, ID: msgid}
|
||||
|
||||
// check if we have an action event
|
||||
var event string
|
||||
if strings.Contains(v.Text, "has set the subject to:") {
|
||||
event = config.EventTopicChange
|
||||
}
|
||||
|
||||
msgID := v.ID
|
||||
if v.ReplaceID != "" {
|
||||
msgID = v.ReplaceID
|
||||
}
|
||||
rmsg := config.Message{
|
||||
Username: b.parseNick(v.Remote),
|
||||
Text: v.Text,
|
||||
Channel: b.parseChannel(v.Remote),
|
||||
Account: b.Account,
|
||||
UserID: v.Remote,
|
||||
ID: msgID,
|
||||
Event: event,
|
||||
}
|
||||
|
||||
// Check if we have an action event.
|
||||
var ok bool
|
||||
rmsg.Text, ok = b.replaceAction(rmsg.Text)
|
||||
if ok {
|
||||
rmsg.Event = config.EventUserAction
|
||||
}
|
||||
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
case xmpp.Presence:
|
||||
// do nothing
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,30 +264,41 @@ func (b *Bxmpp) replaceAction(text string) (string, bool) {
|
||||
}
|
||||
|
||||
// handleUploadFile handles native upload of files
|
||||
func (b *Bxmpp) handleUploadFile(msg *config.Message) (string, error) {
|
||||
var urldesc = ""
|
||||
func (b *Bxmpp) handleUploadFile(msg *config.Message) error {
|
||||
var urlDesc string
|
||||
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Comment != "" {
|
||||
msg.Text += fi.Comment + ": "
|
||||
for _, file := range msg.Extra["file"] {
|
||||
fileInfo := file.(config.FileInfo)
|
||||
if fileInfo.Comment != "" {
|
||||
msg.Text += fileInfo.Comment + ": "
|
||||
}
|
||||
if fi.URL != "" {
|
||||
msg.Text = fi.URL
|
||||
if fi.Comment != "" {
|
||||
msg.Text = fi.Comment + ": " + fi.URL
|
||||
urldesc = fi.Comment
|
||||
if fileInfo.URL != "" {
|
||||
msg.Text = fileInfo.URL
|
||||
if fileInfo.Comment != "" {
|
||||
msg.Text = fileInfo.Comment + ": " + fileInfo.URL
|
||||
urlDesc = fileInfo.Comment
|
||||
}
|
||||
}
|
||||
_, err := b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text})
|
||||
if err != nil {
|
||||
return "", err
|
||||
if _, err := b.xc.Send(xmpp.Chat{
|
||||
Type: "groupchat",
|
||||
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 {
|
||||
@@ -259,7 +337,23 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// do not show subjects on connect #732
|
||||
if strings.Contains(message.Text, "has set the subject to:") && time.Since(b.startTime) < time.Second*5 {
|
||||
return true
|
||||
}
|
||||
|
||||
// skip delayed messages
|
||||
t := time.Time{}
|
||||
return message.Stamp != t
|
||||
return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
@@ -17,6 +19,7 @@ type Bzulip struct {
|
||||
bot *gzb.Bot
|
||||
streams map[int]string
|
||||
*bridge.Config
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func New(cfg *bridge.Config) bridge.Bridger {
|
||||
@@ -100,14 +103,46 @@ func (b *Bzulip) getChannel(id int) string {
|
||||
|
||||
func (b *Bzulip) handleQueue() error {
|
||||
for {
|
||||
messages, _ := b.q.GetEvents()
|
||||
messages, err := b.q.GetEvents()
|
||||
switch err {
|
||||
case gzb.BackoffError:
|
||||
time.Sleep(time.Second * 5)
|
||||
case gzb.NoJSONError:
|
||||
b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds")
|
||||
time.Sleep(time.Second * 10)
|
||||
case gzb.BadEventQueueError:
|
||||
b.Log.Info("got a bad event queue id error, reconnecting")
|
||||
b.bot.Queues = nil
|
||||
for {
|
||||
b.q, err = b.bot.RegisterAll()
|
||||
if err != nil {
|
||||
b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
break
|
||||
}
|
||||
case gzb.HeartbeatError:
|
||||
b.Log.Debug("heartbeat received.")
|
||||
default:
|
||||
b.Log.Debugf("receiving error: %#v", err)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, m := range messages {
|
||||
b.Log.Debugf("== Receiving %#v", m)
|
||||
// ignore our own messages
|
||||
if m.SenderEmail == b.GetString("login") {
|
||||
continue
|
||||
}
|
||||
rmsg := config.Message{Username: m.SenderFullName, Text: m.Content, Channel: b.getChannel(m.StreamID), Account: b.Account, UserID: strconv.Itoa(m.SenderID), Avatar: m.AvatarURL}
|
||||
rmsg := config.Message{
|
||||
Username: m.SenderFullName,
|
||||
Text: m.Content,
|
||||
Channel: b.getChannel(m.StreamID) + "/topic:" + m.Subject,
|
||||
Account: b.Account,
|
||||
UserID: strconv.Itoa(m.SenderID),
|
||||
Avatar: m.AvatarURL,
|
||||
}
|
||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
|
||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
@@ -118,9 +153,11 @@ func (b *Bzulip) handleQueue() error {
|
||||
}
|
||||
|
||||
func (b *Bzulip) sendMessage(msg config.Message) (string, error) {
|
||||
topic := "matterbridge"
|
||||
if b.GetString("topic") != "" {
|
||||
topic = b.GetString("topic")
|
||||
topic := ""
|
||||
if strings.Contains(msg.Channel, "/topic:") {
|
||||
res := strings.Split(msg.Channel, "/topic:")
|
||||
topic = res[1]
|
||||
msg.Channel = res[0]
|
||||
}
|
||||
m := gzb.Message{
|
||||
Stream: msg.Channel,
|
||||
|
||||
179
changelog.md
179
changelog.md
@@ -1,3 +1,182 @@
|
||||
# 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
|
||||
|
||||
This release couldn't exist without the following contributors:
|
||||
@42wim, @qaisjp, @joohoi
|
||||
|
||||
# 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
|
||||
* whatsapp: new protocol added. Add initial WhatsApp support (#711) Thanks to @KrzysztofMadejski
|
||||
* facebook messenger: new protocol via matterbridge api. See https://github.com/VictorNine/fbridge/ for more information.
|
||||
* general: Add scripting (tengo) support for every incoming message (#731). See `TengoModifyMessage`
|
||||
* general: Allow regexs in ignoreNicks. Closes #690 (#720)
|
||||
* general: Support rewriting messages from relaybots using ExtractNicks. Fixes #466 (#730). See `ExtractNicks` in matterbridge.toml.sample
|
||||
* general: refactor Make all loggers derive from non-default instance (#728). Thanks to @Helcaraxan
|
||||
* rocketchat: add support for the rocketchat API. Sending to rocketchat now supports uploading of files, editing and deleting of messages.
|
||||
* discord: Support join/leaves from discord. Closes #654 (#721)
|
||||
* discord: Allow sending discriminator with Discord username (#726). See `UseDiscriminator` in matterbridge.toml.sample
|
||||
* slack: Add extra debug option (slack). See `Debug` in the slack section in matterbridge.toml.sample
|
||||
* telegram: Add support for URL in messageEntities (telegram). Fixes #735 (#736)
|
||||
* telegram: Add MediaConvertWebPToPNG option (telegram). (#741). See `MediaConvertWebPToPNG` in matterbridge.toml.sample
|
||||
|
||||
## Enhancements
|
||||
* general: Fail gracefully on incorrect human input. Fixes #739 (#740)
|
||||
* matrix: Detect html nicks in RemoteNickFormat (matrix). Fixes #696 (#719)
|
||||
* matrix: Send notices on join/parts (matrix). Fixes #712 (#716)
|
||||
|
||||
## Bugfix
|
||||
* general: Handle file upload/download only once for each message (#742)
|
||||
* zulip: Fix error handling on bad event queue id (zulip). Closes #694
|
||||
* zulip: Keep reconnecting until succeed (zulip) (#737)
|
||||
* irc: add support for (older) unrealircd versions. #708
|
||||
* irc: Support quits from irc correctly. Fixes #722 (#724)
|
||||
* matrix: Send username when uploading video/images (matrix). Fixes #715 (#717)
|
||||
* matrix: Trim <p> and </p> tags (matrix). Closes #686 (#753)
|
||||
* slack: Hint at thread replies when messages are unthreaded (slack) (#684)
|
||||
* slack: Fix race-condition in populateUser() (#767)
|
||||
* xmpp: Do not send topic changes on connect (xmpp). Fixes #732 (#733)
|
||||
* telegram: Fix regression in HTML handling (telegram). Closes #734
|
||||
* discord: Do not relay any bot messages (discord) (#743)
|
||||
* rocketchat: Do not send duplicate messages (rocketchat). Fixes #745 (#752)
|
||||
|
||||
## Contributors
|
||||
This release couldn't exist without the following contributors:
|
||||
@Helcaraxan, @KrzysztofMadejski, @AJolly, @DeclanHoare
|
||||
|
||||
# v1.13.1
|
||||
|
||||
This release fixes go modules issues because of https://github.com/labstack/echo/issues/1272
|
||||
|
||||
## Bugfix
|
||||
* general: fixes Unable to build 1.13.0 #698
|
||||
* api: move to labstack/echo/v4 fixes #698
|
||||
|
||||
# v1.13.0
|
||||
|
||||
## New features
|
||||
* general: refactors of telegram, irc, mattermost, matrix, discord, sshchat bridges and the gateway.
|
||||
* irc: Add option to send RAW commands after connection (irc) #490. See `RunCommands` in matterbridge.toml.sample
|
||||
* mattermost: 3.x support dropped
|
||||
* mattermost: Add support for mattermost threading (#627)
|
||||
* slack: Sync channel topics between Slack bridges #585. See `SyncTopic` in matterbridge.toml.sample
|
||||
* matrix: Add support for markdown to HTML conversion (matrix). Closes #663 (#670)
|
||||
* discord: Improve error reporting on failure to join Discord. Fixes #672 (#680)
|
||||
* discord: Use only one webhook if possible (discord) (#681)
|
||||
* discord: Allow to bridge non-bot Discord users (discord) (#689) If you prefix a token with `User ` it'll treat is as a user token.
|
||||
|
||||
## Bugfix
|
||||
* slack: Try downloading files again if slack is too slow (slack). Closes #655 (#656)
|
||||
* slack: Ignore LatencyReport event (slack)
|
||||
* slack: Fix #668 strip lang in code fences sent to Slack (#673)
|
||||
* sshchat: Fix sshchat connection logic (#661)
|
||||
* sshchat: set quiet mode to filter joins/quits
|
||||
* sshchat: Trim newlines in the end of relayed messages
|
||||
* sshchat: fix media links
|
||||
* sshchat: do not relay "Rate limiting is in effect" message
|
||||
* mattermost: Fail if channel starts with hashtag (mattermost). Closes #625
|
||||
* discord: Add file comment to webhook messages (discord). Fixes #358
|
||||
* matrix: Fix displaying usernames for plain text clients. (matrix) (#685)
|
||||
* irc: Fix possible data race (irc). Closes #693
|
||||
* irc: Handle servers without MOTD (irc). Closes #692
|
||||
|
||||
# v1.12.3
|
||||
## Bugfix
|
||||
* slack: Fix bot (legacy token) messages not being send. Closes #571
|
||||
* slack: Populate user on channel join (slack) (#644)
|
||||
* slack: Add wait option for populateUsers/Channels (slack) Fixes #579 (#653)
|
||||
|
||||
# v1.12.2
|
||||
|
||||
## Bugfix
|
||||
* irc: Fix multiple channel join regression. Closes #639
|
||||
* slack: Make slack-legacy change less restrictive (#626)
|
||||
|
||||
# v1.12.1
|
||||
|
||||
## Bugfix
|
||||
* discord: fix regression on server ID connection #619 #617
|
||||
* discord: Limit discord username via webhook to 32 chars
|
||||
* slack: Make sure threaded files stay in thread (slack). Fixes #590
|
||||
* slack: Do not post empty messages (slack). Fixes #574
|
||||
* slack: Handle deleted/edited thread starting messages (slack). Fixes #600 (#605)
|
||||
* irc: Rework connection logic (irc)
|
||||
* irc: Fix Nickserv logic (irc) #602
|
||||
|
||||
# v1.12.0
|
||||
|
||||
## Breaking changes
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/bin/bash
|
||||
go version | grep go1.11 || exit
|
||||
#!/usr/bin/env bash
|
||||
set -u -e -x -o pipefail
|
||||
|
||||
go version | grep go1.12 || exit
|
||||
|
||||
VERSION=$(git describe --tags)
|
||||
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
|
||||
|
||||
17
ci/lint.sh
Executable file
17
ci/lint.sh
Executable 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
17
ci/test.sh
Executable 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
|
||||
210
contrib/api.yaml
Normal file
210
contrib/api.yaml
Normal file
@@ -0,0 +1,210 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
contact: {}
|
||||
description: A read/write API for the Matterbridge chat bridge.
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: 'https://github.com/42wim/matterbridge/blob/master/LICENSE'
|
||||
title: Matterbridge API
|
||||
version: "0.1.0-oas3"
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: string
|
||||
summary: Checks if the server is alive.
|
||||
/message:
|
||||
post:
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/config.OutgoingMessageResponse'
|
||||
summary: Create a message
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/config.OutgoingMessage'
|
||||
description: Message object to create
|
||||
required: true
|
||||
/messages:
|
||||
get:
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/components/schemas/config.IncomingMessage'
|
||||
type: array
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: List new messages
|
||||
/stream:
|
||||
get:
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/x-json-stream:
|
||||
schema:
|
||||
$ref: '#/components/schemas/config.IncomingMessage'
|
||||
summary: Stream realtime messages
|
||||
servers:
|
||||
- url: /api
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
schemas:
|
||||
config.IncomingMessage:
|
||||
properties:
|
||||
avatar:
|
||||
description: URL to an avatar image
|
||||
example: >-
|
||||
https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
|
||||
type: string
|
||||
event:
|
||||
description: >-
|
||||
A specific matterbridge event. (see
|
||||
https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
|
||||
type: string
|
||||
gateway:
|
||||
description: Name of the gateway as configured in matterbridge.toml
|
||||
example: mygateway
|
||||
type: string
|
||||
text:
|
||||
description: Content of the message
|
||||
example: 'Testing, testing, 1-2-3.'
|
||||
type: string
|
||||
username:
|
||||
description: Human-readable username
|
||||
example: alice
|
||||
type: string
|
||||
account:
|
||||
description: Unique account name of format "[protocol].[slug]" as defined in matterbridge.toml
|
||||
example: slack.myteam
|
||||
type: string
|
||||
channel:
|
||||
description: Human-readable channel name of sending bridge
|
||||
example: test-channel
|
||||
type: string
|
||||
id:
|
||||
description: Unique ID of message on the gateway
|
||||
example: slack 1541361213.030700
|
||||
type: string
|
||||
parent_id:
|
||||
description: Unique ID of a parent message, if threaded
|
||||
example: slack 1541361213.030700
|
||||
type: string
|
||||
protocol:
|
||||
description: Chat protocol of the sending bridge
|
||||
example: slack
|
||||
type: string
|
||||
timestamp:
|
||||
description: Timestamp of the message
|
||||
example: "1541361213.030700"
|
||||
type: string
|
||||
userid:
|
||||
description: Userid on the sending bridge
|
||||
example: U4MCXJKNC
|
||||
type: string
|
||||
extra:
|
||||
description: Extra data that doesn't fit in other fields (eg base64 encoded files)
|
||||
type: object
|
||||
config.OutgoingMessage:
|
||||
properties:
|
||||
avatar:
|
||||
description: URL to an avatar image
|
||||
example: >-
|
||||
https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
|
||||
type: string
|
||||
event:
|
||||
description: >-
|
||||
A specific matterbridge event. (see
|
||||
https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
|
||||
example: ""
|
||||
type: string
|
||||
gateway:
|
||||
description: Name of the gateway as configured in matterbridge.toml
|
||||
example: mygateway
|
||||
type: string
|
||||
text:
|
||||
description: Content of the message
|
||||
example: 'Testing, testing, 1-2-3.'
|
||||
type: string
|
||||
username:
|
||||
description: Human-readable username
|
||||
example: alice
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- gateway
|
||||
- text
|
||||
- username
|
||||
config.OutgoingMessageResponse:
|
||||
properties:
|
||||
avatar:
|
||||
description: URL to an avatar image
|
||||
example: >-
|
||||
https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
|
||||
type: string
|
||||
event:
|
||||
description: >-
|
||||
A specific matterbridge event. (see
|
||||
https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
|
||||
example: ""
|
||||
type: string
|
||||
gateway:
|
||||
description: Name of the gateway as configured in matterbridge.toml
|
||||
example: mygateway
|
||||
type: string
|
||||
text:
|
||||
description: Content of the message
|
||||
example: 'Testing, testing, 1-2-3.'
|
||||
type: string
|
||||
username:
|
||||
description: Human-readable username
|
||||
example: alice
|
||||
type: string
|
||||
account:
|
||||
description: fixed api account
|
||||
example: api.local
|
||||
type: string
|
||||
channel:
|
||||
description: fixed api channel
|
||||
example: api
|
||||
type: string
|
||||
id:
|
||||
example: ""
|
||||
type: string
|
||||
parent_id:
|
||||
example: ""
|
||||
type: string
|
||||
protocol:
|
||||
description: fixed api protocol
|
||||
example: api
|
||||
type: string
|
||||
timestamp:
|
||||
description: Timestamp of the message
|
||||
example: "1541361213.030700"
|
||||
type: string
|
||||
userid:
|
||||
example: ""
|
||||
type: string
|
||||
extra:
|
||||
example: null
|
||||
type: object
|
||||
type: object
|
||||
security:
|
||||
- bearerAuth: []
|
||||
2
contrib/example.tengo
Normal file
2
contrib/example.tengo
Normal file
@@ -0,0 +1,2 @@
|
||||
text := import("text")
|
||||
msgText=text.re_replace("matterbridge",msgText,"matterbridge (https://github.com/42wim/matterbridge)")
|
||||
7
contrib/outmessage-irccolors.tengo
Normal file
7
contrib/outmessage-irccolors.tengo
Normal 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,"")
|
||||
}
|
||||
16
contrib/remotenickformat-zerowidth.tengo
Normal file
16
contrib/remotenickformat-zerowidth.tengo
Normal 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:]
|
||||
}
|
||||
9
contrib/remotenickformat.tengo
Normal file
9
contrib/remotenickformat.tengo
Normal 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)
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
FROM cmosh/alpine-arm:edge
|
||||
ENTRYPOINT ["/bin/matterbridge"]
|
||||
FROM alpine:edge as certs
|
||||
RUN apk --update add ca-certificates
|
||||
|
||||
COPY . /go/src/github.com/42wim/matterbridge
|
||||
RUN apk update && apk add go git gcc musl-dev ca-certificates \
|
||||
&& cd /go/src/github.com/42wim/matterbridge \
|
||||
&& export GOPATH=/go \
|
||||
&& go get \
|
||||
&& go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge \
|
||||
&& rm -rf /go \
|
||||
&& apk del --purge git go gcc musl-dev
|
||||
FROM scratch
|
||||
ARG VERSION=1.12.3
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-linux-arm /bin/matterbridge
|
||||
RUN chmod +x /bin/matterbridge
|
||||
ENTRYPOINT ["/bin/matterbridge"]
|
||||
|
||||
5
gateway/bench.tengo
Normal file
5
gateway/bench.tengo
Normal file
@@ -0,0 +1,5 @@
|
||||
text := import("text")
|
||||
if text.re_match("blah",msgText) {
|
||||
msgText="replaced by this"
|
||||
msgUsername="fakeuser"
|
||||
}
|
||||
43
gateway/bridgemap/bridgemap.go
Normal file
43
gateway/bridgemap/bridgemap.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package bridgemap
|
||||
|
||||
import (
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/api"
|
||||
bdiscord "github.com/42wim/matterbridge/bridge/discord"
|
||||
bgitter "github.com/42wim/matterbridge/bridge/gitter"
|
||||
birc "github.com/42wim/matterbridge/bridge/irc"
|
||||
bmatrix "github.com/42wim/matterbridge/bridge/matrix"
|
||||
bmattermost "github.com/42wim/matterbridge/bridge/mattermost"
|
||||
brocketchat "github.com/42wim/matterbridge/bridge/rocketchat"
|
||||
bslack "github.com/42wim/matterbridge/bridge/slack"
|
||||
bsshchat "github.com/42wim/matterbridge/bridge/sshchat"
|
||||
bsteam "github.com/42wim/matterbridge/bridge/steam"
|
||||
btelegram "github.com/42wim/matterbridge/bridge/telegram"
|
||||
bwhatsapp "github.com/42wim/matterbridge/bridge/whatsapp"
|
||||
bxmpp "github.com/42wim/matterbridge/bridge/xmpp"
|
||||
bzulip "github.com/42wim/matterbridge/bridge/zulip"
|
||||
)
|
||||
|
||||
var (
|
||||
FullMap = map[string]bridge.Factory{
|
||||
"api": api.New,
|
||||
"discord": bdiscord.New,
|
||||
"gitter": bgitter.New,
|
||||
"irc": birc.New,
|
||||
"mattermost": bmattermost.New,
|
||||
"matrix": bmatrix.New,
|
||||
"rocketchat": brocketchat.New,
|
||||
"slack-legacy": bslack.NewLegacy,
|
||||
"slack": bslack.New,
|
||||
"sshchat": bsshchat.New,
|
||||
"steam": bsteam.New,
|
||||
"telegram": btelegram.New,
|
||||
"whatsapp": bwhatsapp.New,
|
||||
"xmpp": bxmpp.New,
|
||||
"zulip": bzulip.New,
|
||||
}
|
||||
|
||||
UserTypingSupport = map[string]struct{}{
|
||||
"slack": {},
|
||||
}
|
||||
)
|
||||
@@ -1,35 +1,20 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/api"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
bdiscord "github.com/42wim/matterbridge/bridge/discord"
|
||||
bgitter "github.com/42wim/matterbridge/bridge/gitter"
|
||||
birc "github.com/42wim/matterbridge/bridge/irc"
|
||||
bmatrix "github.com/42wim/matterbridge/bridge/matrix"
|
||||
bmattermost "github.com/42wim/matterbridge/bridge/mattermost"
|
||||
brocketchat "github.com/42wim/matterbridge/bridge/rocketchat"
|
||||
bslack "github.com/42wim/matterbridge/bridge/slack"
|
||||
bsshchat "github.com/42wim/matterbridge/bridge/sshchat"
|
||||
bsteam "github.com/42wim/matterbridge/bridge/steam"
|
||||
btelegram "github.com/42wim/matterbridge/bridge/telegram"
|
||||
bxmpp "github.com/42wim/matterbridge/bridge/xmpp"
|
||||
bzulip "github.com/42wim/matterbridge/bridge/zulip"
|
||||
"github.com/hashicorp/golang-lru"
|
||||
"github.com/42wim/matterbridge/internal"
|
||||
"github.com/d5/tengo/script"
|
||||
"github.com/d5/tengo/stdlib"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
"github.com/peterhellberg/emojilib"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Gateway struct {
|
||||
@@ -43,6 +28,8 @@ type Gateway struct {
|
||||
Message chan config.Message
|
||||
Name string
|
||||
Messages *lru.Cache
|
||||
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
type BrMsgID struct {
|
||||
@@ -51,40 +38,30 @@ type BrMsgID struct {
|
||||
ChannelID string
|
||||
}
|
||||
|
||||
var flog *log.Entry
|
||||
const apiProtocol = "api"
|
||||
|
||||
var bridgeMap = map[string]bridge.Factory{
|
||||
"api": api.New,
|
||||
"discord": bdiscord.New,
|
||||
"gitter": bgitter.New,
|
||||
"irc": birc.New,
|
||||
"mattermost": bmattermost.New,
|
||||
"matrix": bmatrix.New,
|
||||
"rocketchat": brocketchat.New,
|
||||
"slack-legacy": bslack.NewLegacy,
|
||||
"slack": bslack.New,
|
||||
"sshchat": bsshchat.New,
|
||||
"steam": bsteam.New,
|
||||
"telegram": btelegram.New,
|
||||
"xmpp": bxmpp.New,
|
||||
"zulip": bzulip.New,
|
||||
}
|
||||
// New creates a new Gateway object associated with the specified router and
|
||||
// following the given configuration.
|
||||
func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway {
|
||||
logger := rootLogger.WithFields(logrus.Fields{"prefix": "gateway"})
|
||||
|
||||
const (
|
||||
apiProtocol = "api"
|
||||
)
|
||||
|
||||
func New(cfg config.Gateway, r *Router) *Gateway {
|
||||
flog = log.WithFields(log.Fields{"prefix": "gateway"})
|
||||
gw := &Gateway{Channels: make(map[string]*config.ChannelInfo), Message: r.Message,
|
||||
Router: r, Bridges: make(map[string]*bridge.Bridge), Config: r.Config}
|
||||
cache, _ := lru.New(5000)
|
||||
gw.Messages = cache
|
||||
gw.AddConfig(&cfg)
|
||||
gw := &Gateway{
|
||||
Channels: make(map[string]*config.ChannelInfo),
|
||||
Message: r.Message,
|
||||
Router: r,
|
||||
Bridges: make(map[string]*bridge.Bridge),
|
||||
Config: r.Config,
|
||||
Messages: cache,
|
||||
logger: logger,
|
||||
}
|
||||
if err := gw.AddConfig(cfg); err != nil {
|
||||
logger.Errorf("Failed to add configuration to gateway: %#v", err)
|
||||
}
|
||||
return gw
|
||||
}
|
||||
|
||||
// Find the canonical ID that the message is keyed under in cache
|
||||
// FindCanonicalMsgID returns the ID under which a message was stored in the cache.
|
||||
func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
|
||||
ID := protocol + " " + mID
|
||||
if gw.Messages.Contains(ID) {
|
||||
@@ -104,27 +81,36 @@ func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// AddBridge sets up a new bridge in the gateway object with the specified configuration.
|
||||
func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
|
||||
br := gw.Router.getBridge(cfg.Account)
|
||||
if br == nil {
|
||||
br = bridge.New(cfg)
|
||||
br.Config = gw.Router.Config
|
||||
br.General = &gw.BridgeValues().General
|
||||
// set logging
|
||||
br.Log = log.WithFields(log.Fields{"prefix": "bridge"})
|
||||
brconfig := &bridge.Config{Remote: gw.Message, Log: log.WithFields(log.Fields{"prefix": br.Protocol}), Bridge: br}
|
||||
br.Log = gw.logger.WithFields(logrus.Fields{"prefix": br.Protocol})
|
||||
brconfig := &bridge.Config{
|
||||
Remote: gw.Message,
|
||||
Bridge: br,
|
||||
}
|
||||
// add the actual bridger for this protocol to this bridge using the bridgeMap
|
||||
br.Bridger = bridgeMap[br.Protocol](brconfig)
|
||||
if _, ok := gw.Router.BridgeMap[br.Protocol]; !ok {
|
||||
gw.logger.Fatalf("Incorrect protocol %s specified in gateway configuration %s, exiting.", br.Protocol, cfg.Account)
|
||||
}
|
||||
br.Bridger = gw.Router.BridgeMap[br.Protocol](brconfig)
|
||||
}
|
||||
gw.mapChannelsToBridge(br)
|
||||
gw.Bridges[cfg.Account] = br
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddConfig associates a new configuration with the gateway object.
|
||||
func (gw *Gateway) AddConfig(cfg *config.Gateway) error {
|
||||
gw.Name = cfg.Name
|
||||
gw.MyConfig = cfg
|
||||
gw.mapChannels()
|
||||
if err := gw.mapChannels(); err != nil {
|
||||
gw.logger.Errorf("mapChannels() failed: %s", err)
|
||||
}
|
||||
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
|
||||
br := br //scopelint
|
||||
err := gw.AddBridge(&br)
|
||||
@@ -144,18 +130,22 @@ func (gw *Gateway) mapChannelsToBridge(br *bridge.Bridge) {
|
||||
}
|
||||
|
||||
func (gw *Gateway) reconnectBridge(br *bridge.Bridge) {
|
||||
br.Disconnect()
|
||||
if err := br.Disconnect(); err != nil {
|
||||
gw.logger.Errorf("Disconnect() %s failed: %s", br.Account, err)
|
||||
}
|
||||
time.Sleep(time.Second * 5)
|
||||
RECONNECT:
|
||||
flog.Infof("Reconnecting %s", br.Account)
|
||||
gw.logger.Infof("Reconnecting %s", br.Account)
|
||||
err := br.Connect()
|
||||
if err != nil {
|
||||
flog.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
|
||||
gw.logger.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
|
||||
time.Sleep(time.Second * 60)
|
||||
goto RECONNECT
|
||||
}
|
||||
br.Joined = make(map[string]bool)
|
||||
br.JoinChannels()
|
||||
if err := br.JoinChannels(); err != nil {
|
||||
gw.logger.Errorf("JoinChannels() %s failed: %s", br.Account, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) {
|
||||
@@ -167,10 +157,24 @@ func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) {
|
||||
if strings.HasPrefix(br.Account, "irc.") {
|
||||
br.Channel = strings.ToLower(br.Channel)
|
||||
}
|
||||
if strings.HasPrefix(br.Account, "mattermost.") && strings.HasPrefix(br.Channel, "#") {
|
||||
gw.logger.Errorf("Mattermost channels do not start with a #: remove the # in %s", br.Channel)
|
||||
os.Exit(1)
|
||||
}
|
||||
if strings.HasPrefix(br.Account, "zulip.") && !strings.Contains(br.Channel, "/topic:") {
|
||||
gw.logger.Errorf("Breaking change, since matterbridge 1.14.0 zulip channels need to specify the topic with channel/topic:mytopic in %s of %s", br.Channel, br.Account)
|
||||
os.Exit(1)
|
||||
}
|
||||
ID := br.Channel + br.Account
|
||||
if _, ok := gw.Channels[ID]; !ok {
|
||||
channel := &config.ChannelInfo{Name: br.Channel, Direction: direction, ID: ID, Options: br.Options, Account: br.Account,
|
||||
SameChannel: make(map[string]bool)}
|
||||
channel := &config.ChannelInfo{
|
||||
Name: br.Channel,
|
||||
Direction: direction,
|
||||
ID: ID,
|
||||
Options: br.Options,
|
||||
Account: br.Account,
|
||||
SameChannel: make(map[string]bool),
|
||||
}
|
||||
channel.SameChannel[gw.Name] = br.SameChannel
|
||||
gw.Channels[channel.ID] = channel
|
||||
} else {
|
||||
@@ -198,10 +202,21 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
|
||||
return channels
|
||||
}
|
||||
|
||||
// discord join/leave is for the whole bridge, isn't a per channel join/leave
|
||||
if msg.Event == config.EventJoinLeave && getProtocol(msg) == "discord" && msg.Channel == "" {
|
||||
for _, channel := range gw.Channels {
|
||||
if channel.Account == dest.Account && strings.Contains(channel.Direction, "out") &&
|
||||
gw.validGatewayDest(msg) {
|
||||
channels = append(channels, *channel)
|
||||
}
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
// if source channel is in only, do nothing
|
||||
for _, channel := range gw.Channels {
|
||||
// lookup the channel from the message
|
||||
if channel.ID == getChannelID(*msg) {
|
||||
if channel.ID == getChannelID(msg) {
|
||||
// we only have destinations if the original message is from an "in" (sending) channel
|
||||
if !strings.Contains(channel.Direction, "in") {
|
||||
return channels
|
||||
@@ -210,11 +225,11 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
|
||||
}
|
||||
}
|
||||
for _, channel := range gw.Channels {
|
||||
if _, ok := gw.Channels[getChannelID(*msg)]; !ok {
|
||||
if _, ok := gw.Channels[getChannelID(msg)]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// do samechannelgateway flogic
|
||||
// do samechannelgateway logic
|
||||
if channel.SameChannel[msg.Gateway] {
|
||||
if msg.Channel == channel.Name && msg.Account != dest.Account {
|
||||
channels = append(channels, *channel)
|
||||
@@ -228,7 +243,7 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
|
||||
return channels
|
||||
}
|
||||
|
||||
func (gw *Gateway) getDestMsgID(msgID string, dest *bridge.Bridge, channel config.ChannelInfo) string {
|
||||
func (gw *Gateway) getDestMsgID(msgID string, dest *bridge.Bridge, channel *config.ChannelInfo) string {
|
||||
if res, ok := gw.Messages.Get(msgID); ok {
|
||||
IDs := res.([]*BrMsgID)
|
||||
for _, id := range IDs {
|
||||
@@ -242,103 +257,23 @@ func (gw *Gateway) getDestMsgID(msgID string, dest *bridge.Bridge, channel confi
|
||||
return ""
|
||||
}
|
||||
|
||||
func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrMsgID {
|
||||
var brMsgIDs []*BrMsgID
|
||||
|
||||
// if we have an attached file, or other info
|
||||
if msg.Extra != nil {
|
||||
if len(msg.Extra[config.EventFileFailureSize]) != 0 {
|
||||
if msg.Text == "" {
|
||||
return brMsgIDs
|
||||
}
|
||||
}
|
||||
// ignoreTextEmpty returns true if we need to ignore a message with an empty text.
|
||||
func (gw *Gateway) ignoreTextEmpty(msg *config.Message) bool {
|
||||
if msg.Text != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Avatar downloads are only relevant for telegram and mattermost for now
|
||||
if msg.Event == config.EventAvatarDownload {
|
||||
if dest.Protocol != "mattermost" &&
|
||||
dest.Protocol != "telegram" {
|
||||
return brMsgIDs
|
||||
}
|
||||
if msg.Event == config.EventUserTyping {
|
||||
return false
|
||||
}
|
||||
|
||||
// only relay join/part when configured
|
||||
if msg.Event == config.EventJoinLeave && !gw.Bridges[dest.Account].GetBool("ShowJoinPart") {
|
||||
return brMsgIDs
|
||||
// we have an attachment or actual bytes, do not ignore
|
||||
if msg.Extra != nil &&
|
||||
(msg.Extra["attachments"] != nil ||
|
||||
len(msg.Extra["file"]) > 0 ||
|
||||
len(msg.Extra[config.EventFileFailureSize]) > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// only relay topic change when configured
|
||||
if msg.Event == config.EventTopicChange && !gw.Bridges[dest.Account].GetBool("ShowTopicChange") {
|
||||
return brMsgIDs
|
||||
}
|
||||
|
||||
// broadcast to every out channel (irc QUIT)
|
||||
if msg.Channel == "" && msg.Event != config.EventJoinLeave {
|
||||
flog.Debug("empty channel")
|
||||
return brMsgIDs
|
||||
}
|
||||
|
||||
// Get the ID of the parent message in thread
|
||||
var canonicalParentMsgID string
|
||||
if msg.ParentID != "" && (gw.BridgeValues().General.PreserveThreading || dest.GetBool("PreserveThreading")) {
|
||||
canonicalParentMsgID = gw.FindCanonicalMsgID(msg.Protocol, msg.ParentID)
|
||||
}
|
||||
|
||||
originchannel := msg.Channel
|
||||
origmsg := msg
|
||||
channels := gw.getDestChannel(&msg, *dest)
|
||||
for _, channel := range channels {
|
||||
// Only send the avatar download event to ourselves.
|
||||
if msg.Event == config.EventAvatarDownload {
|
||||
if channel.ID != getChannelID(origmsg) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// do not send to ourself for any other event
|
||||
if channel.ID == getChannelID(origmsg) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Too noisy to log like other events
|
||||
if msg.Event != config.EventUserTyping {
|
||||
flog.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name)
|
||||
}
|
||||
|
||||
msg.Channel = channel.Name
|
||||
msg.Avatar = gw.modifyAvatar(origmsg, dest)
|
||||
msg.Username = gw.modifyUsername(origmsg, dest)
|
||||
|
||||
msg.ID = gw.getDestMsgID(origmsg.Protocol+" "+origmsg.ID, dest, channel)
|
||||
|
||||
// for api we need originchannel as channel
|
||||
if dest.Protocol == apiProtocol {
|
||||
msg.Channel = originchannel
|
||||
}
|
||||
|
||||
msg.ParentID = gw.getDestMsgID(origmsg.Protocol+" "+canonicalParentMsgID, dest, channel)
|
||||
if msg.ParentID == "" {
|
||||
msg.ParentID = canonicalParentMsgID
|
||||
}
|
||||
|
||||
// if we are using mattermost plugin account, send messages to MattermostPlugin channel
|
||||
// that can be picked up by the mattermost matterbridge plugin
|
||||
if dest.Account == "mattermost.plugin" {
|
||||
gw.Router.MattermostPlugin <- msg
|
||||
}
|
||||
|
||||
mID, err := dest.Send(msg)
|
||||
if err != nil {
|
||||
flog.Error(err)
|
||||
}
|
||||
|
||||
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
|
||||
if mID != "" {
|
||||
flog.Debugf("mID %s: %s", dest.Account, mID)
|
||||
brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID})
|
||||
}
|
||||
}
|
||||
return brMsgIDs
|
||||
gw.logger.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
|
||||
return true
|
||||
}
|
||||
|
||||
func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
|
||||
@@ -347,59 +282,23 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// check if we need to ignore a empty message
|
||||
if msg.Text == "" {
|
||||
if msg.Event == config.EventUserTyping {
|
||||
return false
|
||||
}
|
||||
// we have an attachment or actual bytes, do not ignore
|
||||
if msg.Extra != nil &&
|
||||
(msg.Extra["attachments"] != nil ||
|
||||
len(msg.Extra["file"]) > 0 ||
|
||||
len(msg.Extra[config.EventFileFailureSize]) > 0) {
|
||||
return false
|
||||
}
|
||||
flog.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
|
||||
igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks"))
|
||||
igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages"))
|
||||
if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) {
|
||||
return true
|
||||
}
|
||||
|
||||
// is the username in IgnoreNicks field
|
||||
for _, entry := range strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks")) {
|
||||
if msg.Username == entry {
|
||||
flog.Debugf("ignoring %s from %s", msg.Username, msg.Account)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// does the message match regex in IgnoreMessages field
|
||||
// TODO do not compile regexps everytime
|
||||
for _, entry := range strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages")) {
|
||||
if entry != "" {
|
||||
re, err := regexp.Compile(entry)
|
||||
if err != nil {
|
||||
flog.Errorf("incorrect regexp %s for %s", entry, msg.Account)
|
||||
continue
|
||||
}
|
||||
if re.MatchString(msg.Text) {
|
||||
flog.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) string {
|
||||
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string {
|
||||
br := gw.Bridges[msg.Account]
|
||||
msg.Protocol = br.Protocol
|
||||
if gw.BridgeValues().General.StripNick || dest.GetBool("StripNick") {
|
||||
if dest.GetBool("StripNick") {
|
||||
re := regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
msg.Username = re.ReplaceAllString(msg.Username, "")
|
||||
}
|
||||
nick := dest.GetString("RemoteNickFormat")
|
||||
if nick == "" {
|
||||
nick = gw.BridgeValues().General.RemoteNickFormat
|
||||
}
|
||||
|
||||
// loop to replace nicks
|
||||
for _, outer := range br.GetStringSlice2D("ReplaceNicks") {
|
||||
@@ -408,7 +307,7 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
|
||||
// TODO move compile to bridge init somewhere
|
||||
re, err := regexp.Compile(search)
|
||||
if err != nil {
|
||||
flog.Errorf("regexp in %s failed: %s", msg.Account, err)
|
||||
gw.logger.Errorf("regexp in %s failed: %s", msg.Account, err)
|
||||
break
|
||||
}
|
||||
msg.Username = re.ReplaceAllString(msg.Username, replace)
|
||||
@@ -433,14 +332,16 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin
|
||||
nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1)
|
||||
nick = strings.Replace(nick, "{NICK}", msg.Username, -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
|
||||
}
|
||||
|
||||
func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string {
|
||||
iconurl := gw.BridgeValues().General.IconURL
|
||||
if iconurl == "" {
|
||||
iconurl = dest.GetString("IconURL")
|
||||
}
|
||||
func (gw *Gateway) modifyAvatar(msg *config.Message, dest *bridge.Bridge) string {
|
||||
iconurl := dest.GetString("IconURL")
|
||||
iconurl = strings.Replace(iconurl, "{NICK}", msg.Username, -1)
|
||||
if msg.Avatar == "" {
|
||||
msg.Avatar = iconurl
|
||||
@@ -449,6 +350,13 @@ func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string
|
||||
}
|
||||
|
||||
func (gw *Gateway) modifyMessage(msg *config.Message) {
|
||||
if err := modifyMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil {
|
||||
gw.logger.Errorf("TengoModifyMessage failed: %s", err)
|
||||
}
|
||||
if err := modifyMessageTengo(gw.BridgeValues().Tengo.Message, msg); err != nil {
|
||||
gw.logger.Errorf("Tengo.Message failed: %s", err)
|
||||
}
|
||||
|
||||
// replace :emoji: to unicode
|
||||
msg.Text = emojilib.Replace(msg.Text)
|
||||
|
||||
@@ -460,108 +368,226 @@ func (gw *Gateway) modifyMessage(msg *config.Message) {
|
||||
// TODO move compile to bridge init somewhere
|
||||
re, err := regexp.Compile(search)
|
||||
if err != nil {
|
||||
flog.Errorf("regexp in %s failed: %s", msg.Account, err)
|
||||
gw.logger.Errorf("regexp in %s failed: %s", msg.Account, err)
|
||||
break
|
||||
}
|
||||
msg.Text = re.ReplaceAllString(msg.Text, replace)
|
||||
}
|
||||
|
||||
gw.handleExtractNicks(msg)
|
||||
|
||||
// messages from api have Gateway specified, don't overwrite
|
||||
if msg.Protocol != apiProtocol {
|
||||
msg.Gateway = gw.Name
|
||||
}
|
||||
}
|
||||
|
||||
// handleFiles uploads or places all files on the given msg to the MediaServer and
|
||||
// adds the new URL of the file on the MediaServer onto the given msg.
|
||||
func (gw *Gateway) handleFiles(msg *config.Message) {
|
||||
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
|
||||
// If we don't have a attachfield or we don't have a mediaserver configured return
|
||||
if msg.Extra == nil ||
|
||||
(gw.BridgeValues().General.MediaServerUpload == "" &&
|
||||
gw.BridgeValues().General.MediaDownloadPath == "") {
|
||||
return
|
||||
}
|
||||
|
||||
// If we don't have files, nothing to upload.
|
||||
if len(msg.Extra["file"]) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
|
||||
for i, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
ext := filepath.Ext(fi.Name)
|
||||
fi.Name = fi.Name[0 : len(fi.Name)-len(ext)]
|
||||
fi.Name = reg.ReplaceAllString(fi.Name, "_")
|
||||
fi.Name += ext
|
||||
|
||||
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8]
|
||||
|
||||
if gw.BridgeValues().General.MediaServerUpload != "" {
|
||||
// Use MediaServerUpload. Upload using a PUT HTTP request and basicauth.
|
||||
|
||||
url := gw.BridgeValues().General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
|
||||
|
||||
req, err := http.NewRequest("PUT", url, bytes.NewReader(*fi.Data))
|
||||
if err != nil {
|
||||
flog.Errorf("mediaserver upload failed, could not create request: %#v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
flog.Debugf("mediaserver upload url: %s", url)
|
||||
|
||||
req.Header.Set("Content-Type", "binary/octet-stream")
|
||||
_, err = client.Do(req)
|
||||
if err != nil {
|
||||
flog.Errorf("mediaserver upload failed, could not Do request: %#v", err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Use MediaServerPath. Place the file on the current filesystem.
|
||||
|
||||
dir := gw.BridgeValues().General.MediaDownloadPath + "/" + sha1sum
|
||||
err := os.Mkdir(dir, os.ModePerm)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
flog.Errorf("mediaserver path failed, could not mkdir: %s %#v", err, err)
|
||||
continue
|
||||
}
|
||||
|
||||
path := dir + "/" + fi.Name
|
||||
flog.Debugf("mediaserver path placing file: %s", path)
|
||||
|
||||
err = ioutil.WriteFile(path, *fi.Data, os.ModePerm)
|
||||
if err != nil {
|
||||
flog.Errorf("mediaserver path failed, could not writefile: %s %#v", err, err)
|
||||
continue
|
||||
}
|
||||
// SendMessage sends a message (with specified parentID) to the channel on the selected
|
||||
// destination bridge and returns a message ID or an error.
|
||||
func (gw *Gateway) SendMessage(
|
||||
rmsg *config.Message,
|
||||
dest *bridge.Bridge,
|
||||
channel *config.ChannelInfo,
|
||||
canonicalParentMsgID string,
|
||||
) (string, error) {
|
||||
msg := *rmsg
|
||||
// Only send the avatar download event to ourselves.
|
||||
if msg.Event == config.EventAvatarDownload {
|
||||
if channel.ID != getChannelID(rmsg) {
|
||||
return "", nil
|
||||
}
|
||||
} else {
|
||||
// do not send to ourself for any other event
|
||||
if channel.ID == getChannelID(rmsg) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Download URL.
|
||||
durl := gw.BridgeValues().General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
|
||||
|
||||
flog.Debugf("mediaserver download URL = %s", durl)
|
||||
|
||||
// We uploaded/placed the file successfully. Add the SHA and URL.
|
||||
extra := msg.Extra["file"][i].(config.FileInfo)
|
||||
extra.URL = durl
|
||||
extra.SHA = sha1sum
|
||||
msg.Extra["file"][i] = extra
|
||||
}
|
||||
|
||||
// Too noisy to log like other events
|
||||
if msg.Event != config.EventUserTyping {
|
||||
gw.logger.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name)
|
||||
}
|
||||
|
||||
msg.Channel = channel.Name
|
||||
msg.Avatar = gw.modifyAvatar(rmsg, dest)
|
||||
msg.Username = gw.modifyUsername(rmsg, dest)
|
||||
|
||||
msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel)
|
||||
|
||||
// for api we need originchannel as channel
|
||||
if dest.Protocol == apiProtocol {
|
||||
msg.Channel = rmsg.Channel
|
||||
}
|
||||
|
||||
msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel)
|
||||
if msg.ParentID == "" {
|
||||
msg.ParentID = canonicalParentMsgID
|
||||
}
|
||||
|
||||
// if the parentID is still empty and we have a parentID set in the original message
|
||||
// this means that we didn't find it in the cache so set it "msg-parent-not-found"
|
||||
if msg.ParentID == "" && rmsg.ParentID != "" {
|
||||
msg.ParentID = "msg-parent-not-found"
|
||||
}
|
||||
|
||||
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
|
||||
// that can be picked up by the mattermost matterbridge plugin
|
||||
if dest.Account == "mattermost.plugin" {
|
||||
gw.Router.MattermostPlugin <- msg
|
||||
}
|
||||
|
||||
mID, err := dest.Send(msg)
|
||||
if err != nil {
|
||||
return mID, err
|
||||
}
|
||||
|
||||
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
|
||||
if mID != "" {
|
||||
gw.logger.Debugf("mID %s: %s", dest.Account, mID)
|
||||
return mID, nil
|
||||
//brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID})
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (gw *Gateway) validGatewayDest(msg *config.Message) bool {
|
||||
return msg.Gateway == gw.Name
|
||||
}
|
||||
|
||||
func getChannelID(msg config.Message) string {
|
||||
func getChannelID(msg *config.Message) string {
|
||||
return msg.Channel + msg.Account
|
||||
}
|
||||
|
||||
func isAPI(account string) bool {
|
||||
return strings.HasPrefix(account, "api.")
|
||||
}
|
||||
|
||||
// ignoreText returns true if text matches any of the input regexes.
|
||||
func (gw *Gateway) ignoreText(text string, input []string) bool {
|
||||
for _, entry := range input {
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
// TODO do not compile regexps everytime
|
||||
re, err := regexp.Compile(entry)
|
||||
if err != nil {
|
||||
gw.logger.Errorf("incorrect regexp %s", entry)
|
||||
continue
|
||||
}
|
||||
if re.MatchString(text) {
|
||||
gw.logger.Debugf("matching %s. ignoring %s", entry, text)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getProtocol(msg *config.Message) string {
|
||||
p := strings.Split(msg.Account, ".")
|
||||
return p[0]
|
||||
}
|
||||
|
||||
func modifyMessageTengo(filename string, msg *config.Message) error {
|
||||
if filename == "" {
|
||||
return nil
|
||||
}
|
||||
res, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s := script.New(res)
|
||||
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
|
||||
_ = s.Add("msgText", msg.Text)
|
||||
_ = s.Add("msgUsername", msg.Username)
|
||||
_ = s.Add("msgAccount", msg.Account)
|
||||
_ = s.Add("msgChannel", msg.Channel)
|
||||
c, err := s.Compile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Text = c.Get("msgText").String()
|
||||
msg.Username = c.Get("msgUsername").String()
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@ package gateway
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/gateway/bridgemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"testing"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
var testconfig = []byte(`
|
||||
@@ -159,8 +162,10 @@ const (
|
||||
)
|
||||
|
||||
func maketestRouter(input []byte) *Router {
|
||||
cfg := config.NewConfigFromString(input)
|
||||
r, err := NewRouter(cfg)
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(ioutil.Discard)
|
||||
cfg := config.NewConfigFromString(logger, input)
|
||||
r, err := NewRouter(logger, cfg, bridgemap.FullMap)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
@@ -386,3 +391,139 @@ func TestGetDestChannelAdvanced(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, map[string]int{"bridge3": 4, "bridge": 9, "announcements": 3, "bridge2": 4}, hits)
|
||||
}
|
||||
|
||||
type ignoreTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
gw *Gateway
|
||||
}
|
||||
|
||||
func TestIgnoreSuite(t *testing.T) {
|
||||
s := &ignoreTestSuite{}
|
||||
suite.Run(t, s)
|
||||
}
|
||||
|
||||
func (s *ignoreTestSuite) SetupSuite() {
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(ioutil.Discard)
|
||||
s.gw = &Gateway{logger: logrus.NewEntry(logger)}
|
||||
}
|
||||
func (s *ignoreTestSuite) TestIgnoreTextEmpty() {
|
||||
extraFile := make(map[string][]interface{})
|
||||
extraAttach := make(map[string][]interface{})
|
||||
extraFailure := make(map[string][]interface{})
|
||||
extraFile["file"] = append(extraFile["file"], config.FileInfo{})
|
||||
extraAttach["attachments"] = append(extraAttach["attachments"], []string{})
|
||||
extraFailure[config.EventFileFailureSize] = append(extraFailure[config.EventFileFailureSize], config.FileInfo{})
|
||||
|
||||
msgTests := map[string]struct {
|
||||
input *config.Message
|
||||
output bool
|
||||
}{
|
||||
"usertyping": {
|
||||
input: &config.Message{Event: config.EventUserTyping},
|
||||
output: false,
|
||||
},
|
||||
"file attach": {
|
||||
input: &config.Message{Extra: extraFile},
|
||||
output: false,
|
||||
},
|
||||
"attachments": {
|
||||
input: &config.Message{Extra: extraAttach},
|
||||
output: false,
|
||||
},
|
||||
config.EventFileFailureSize: {
|
||||
input: &config.Message{Extra: extraFailure},
|
||||
output: false,
|
||||
},
|
||||
"nil extra": {
|
||||
input: &config.Message{Extra: nil},
|
||||
output: true,
|
||||
},
|
||||
"empty": {
|
||||
input: &config.Message{},
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
for testname, testcase := range msgTests {
|
||||
output := s.gw.ignoreTextEmpty(testcase.input)
|
||||
s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *ignoreTestSuite) TestIgnoreTexts() {
|
||||
msgTests := map[string]struct {
|
||||
input string
|
||||
re []string
|
||||
output bool
|
||||
}{
|
||||
"no regex": {
|
||||
input: "a text message",
|
||||
re: []string{},
|
||||
output: false,
|
||||
},
|
||||
"simple regex": {
|
||||
input: "a text message",
|
||||
re: []string{"text"},
|
||||
output: true,
|
||||
},
|
||||
"multiple regex fail": {
|
||||
input: "a text message",
|
||||
re: []string{"abc", "123$"},
|
||||
output: false,
|
||||
},
|
||||
"multiple regex pass": {
|
||||
input: "a text message",
|
||||
re: []string{"lala", "sage$"},
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
for testname, testcase := range msgTests {
|
||||
output := s.gw.ignoreText(testcase.input, testcase.re)
|
||||
s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ignoreTestSuite) TestIgnoreNicks() {
|
||||
msgTests := map[string]struct {
|
||||
input string
|
||||
re []string
|
||||
output bool
|
||||
}{
|
||||
"no entry": {
|
||||
input: "user",
|
||||
re: []string{},
|
||||
output: false,
|
||||
},
|
||||
"one entry": {
|
||||
input: "user",
|
||||
re: []string{"user"},
|
||||
output: true,
|
||||
},
|
||||
"multiple entries": {
|
||||
input: "user",
|
||||
re: []string{"abc", "user"},
|
||||
output: true,
|
||||
},
|
||||
"multiple entries fail": {
|
||||
input: "user",
|
||||
re: []string{"abc", "def"},
|
||||
output: false,
|
||||
},
|
||||
}
|
||||
for testname, testcase := range msgTests {
|
||||
output := s.gw.ignoreText(testcase.input, testcase.re)
|
||||
s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTengo(b *testing.B) {
|
||||
msg := &config.Message{Username: "user", Text: "blah testing", Account: "protocol.account", Channel: "mychannel"}
|
||||
for n := 0; n < b.N; n++ {
|
||||
err := modifyMessageTengo("bench.tengo", msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
275
gateway/handlers.go
Normal file
275
gateway/handlers.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1" //nolint:gosec
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/gateway/bridgemap"
|
||||
)
|
||||
|
||||
// handleEventFailure handles failures and reconnects bridges.
|
||||
func (r *Router) handleEventFailure(msg *config.Message) {
|
||||
if msg.Event != config.EventFailure {
|
||||
return
|
||||
}
|
||||
for _, gw := range r.Gateways {
|
||||
for _, br := range gw.Bridges {
|
||||
if msg.Account == br.Account {
|
||||
go gw.reconnectBridge(br)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleEventGetChannelMembers handles channel members
|
||||
func (r *Router) handleEventGetChannelMembers(msg *config.Message) {
|
||||
if msg.Event != config.EventGetChannelMembers {
|
||||
return
|
||||
}
|
||||
for _, gw := range r.Gateways {
|
||||
for _, br := range gw.Bridges {
|
||||
if msg.Account == br.Account {
|
||||
cMembers := msg.Extra[config.EventGetChannelMembers][0].(config.ChannelMembers)
|
||||
r.logger.Debugf("Syncing channelmembers from %s", msg.Account)
|
||||
br.SetChannelMembers(&cMembers)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleEventRejoinChannels handles rejoining of channels.
|
||||
func (r *Router) handleEventRejoinChannels(msg *config.Message) {
|
||||
if msg.Event != config.EventRejoinChannels {
|
||||
return
|
||||
}
|
||||
for _, gw := range r.Gateways {
|
||||
for _, br := range gw.Bridges {
|
||||
if msg.Account == br.Account {
|
||||
br.Joined = make(map[string]bool)
|
||||
if err := br.JoinChannels(); err != nil {
|
||||
r.logger.Errorf("channel join failed for %s: %s", msg.Account, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleFiles uploads or places all files on the given msg to the MediaServer and
|
||||
// adds the new URL of the file on the MediaServer onto the given msg.
|
||||
func (gw *Gateway) handleFiles(msg *config.Message) {
|
||||
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
|
||||
// If we don't have a attachfield or we don't have a mediaserver configured return
|
||||
if msg.Extra == nil ||
|
||||
(gw.BridgeValues().General.MediaServerUpload == "" &&
|
||||
gw.BridgeValues().General.MediaDownloadPath == "") {
|
||||
return
|
||||
}
|
||||
|
||||
// If we don't have files, nothing to upload.
|
||||
if len(msg.Extra["file"]) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
ext := filepath.Ext(fi.Name)
|
||||
fi.Name = fi.Name[0 : len(fi.Name)-len(ext)]
|
||||
fi.Name = reg.ReplaceAllString(fi.Name, "_")
|
||||
fi.Name += ext
|
||||
|
||||
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8] //nolint:gosec
|
||||
|
||||
if gw.BridgeValues().General.MediaServerUpload != "" {
|
||||
// Use MediaServerUpload. Upload using a PUT HTTP request and basicauth.
|
||||
if err := gw.handleFilesUpload(&fi); err != nil {
|
||||
gw.logger.Error(err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Use MediaServerPath. Place the file on the current filesystem.
|
||||
if err := gw.handleFilesLocal(&fi); err != nil {
|
||||
gw.logger.Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Download URL.
|
||||
durl := gw.BridgeValues().General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
|
||||
|
||||
gw.logger.Debugf("mediaserver download URL = %s", durl)
|
||||
|
||||
// We uploaded/placed the file successfully. Add the SHA and URL.
|
||||
extra := msg.Extra["file"][i].(config.FileInfo)
|
||||
extra.URL = durl
|
||||
extra.SHA = sha1sum
|
||||
msg.Extra["file"][i] = extra
|
||||
}
|
||||
}
|
||||
|
||||
// handleFilesUpload uses MediaServerUpload configuration to upload the file.
|
||||
// Returns error on failure.
|
||||
func (gw *Gateway) handleFilesUpload(fi *config.FileInfo) error {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
// Use MediaServerUpload. Upload using a PUT HTTP request and basicauth.
|
||||
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8] //nolint:gosec
|
||||
url := gw.BridgeValues().General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
|
||||
|
||||
req, err := http.NewRequest("PUT", url, bytes.NewReader(*fi.Data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("mediaserver upload failed, could not create request: %#v", err)
|
||||
}
|
||||
|
||||
gw.logger.Debugf("mediaserver upload url: %s", url)
|
||||
|
||||
req.Header.Set("Content-Type", "binary/octet-stream")
|
||||
_, err = client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mediaserver upload failed, could not Do request: %#v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleFilesLocal use MediaServerPath configuration, places the file on the current filesystem.
|
||||
// Returns error on failure.
|
||||
func (gw *Gateway) handleFilesLocal(fi *config.FileInfo) error {
|
||||
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8] //nolint:gosec
|
||||
dir := gw.BridgeValues().General.MediaDownloadPath + "/" + sha1sum
|
||||
err := os.Mkdir(dir, os.ModePerm)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("mediaserver path failed, could not mkdir: %s %#v", err, err)
|
||||
}
|
||||
|
||||
path := dir + "/" + fi.Name
|
||||
gw.logger.Debugf("mediaserver path placing file: %s", path)
|
||||
|
||||
err = ioutil.WriteFile(path, *fi.Data, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mediaserver path failed, could not writefile: %s %#v", err, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ignoreEvent returns true if we need to ignore this event for the specified destination bridge.
|
||||
func (gw *Gateway) ignoreEvent(event string, dest *bridge.Bridge) bool {
|
||||
switch event {
|
||||
case config.EventAvatarDownload:
|
||||
// Avatar downloads are only relevant for telegram and mattermost for now
|
||||
if dest.Protocol != "mattermost" && dest.Protocol != "telegram" {
|
||||
return true
|
||||
}
|
||||
case config.EventJoinLeave:
|
||||
// only relay join/part when configured
|
||||
if !dest.GetBool("ShowJoinPart") {
|
||||
return true
|
||||
}
|
||||
case config.EventTopicChange:
|
||||
// only relay topic change when used in some way on other side
|
||||
if dest.GetBool("ShowTopicChange") && dest.GetBool("SyncTopic") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// handleMessage makes sure the message get sent to the correct bridge/channels.
|
||||
// Returns an array of msg ID's
|
||||
func (gw *Gateway) handleMessage(rmsg *config.Message, dest *bridge.Bridge) []*BrMsgID {
|
||||
var brMsgIDs []*BrMsgID
|
||||
|
||||
// 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 rmsg.Extra != nil && len(rmsg.Extra[config.EventFileFailureSize]) != 0 && rmsg.Text == "" {
|
||||
return brMsgIDs
|
||||
}
|
||||
|
||||
if gw.ignoreEvent(rmsg.Event, dest) {
|
||||
return brMsgIDs
|
||||
}
|
||||
|
||||
// broadcast to every out channel (irc QUIT)
|
||||
if rmsg.Channel == "" && rmsg.Event != config.EventJoinLeave {
|
||||
gw.logger.Debug("empty channel")
|
||||
return brMsgIDs
|
||||
}
|
||||
|
||||
// Get the ID of the parent message in thread
|
||||
var canonicalParentMsgID string
|
||||
if rmsg.ParentID != "" && dest.GetBool("PreserveThreading") {
|
||||
canonicalParentMsgID = gw.FindCanonicalMsgID(rmsg.Protocol, rmsg.ParentID)
|
||||
}
|
||||
|
||||
channels := gw.getDestChannel(rmsg, *dest)
|
||||
for idx := range channels {
|
||||
channel := &channels[idx]
|
||||
msgID, err := gw.SendMessage(rmsg, dest, channel, canonicalParentMsgID)
|
||||
if err != nil {
|
||||
gw.logger.Errorf("SendMessage failed: %s", err)
|
||||
continue
|
||||
}
|
||||
if msgID == "" {
|
||||
continue
|
||||
}
|
||||
brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + msgID, channel.ID})
|
||||
}
|
||||
return brMsgIDs
|
||||
}
|
||||
|
||||
func (gw *Gateway) handleExtractNicks(msg *config.Message) {
|
||||
var err error
|
||||
br := gw.Bridges[msg.Account]
|
||||
for _, outer := range br.GetStringSlice2D("ExtractNicks") {
|
||||
search := outer[0]
|
||||
replace := outer[1]
|
||||
msg.Username, msg.Text, err = extractNick(search, replace, msg.Username, msg.Text)
|
||||
if err != nil {
|
||||
gw.logger.Errorf("regexp in %s failed: %s", msg.Account, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractNick searches for a username (based on "search" a regular expression).
|
||||
// if this matches it extracts a nick (based on "extract" another regular expression) from text
|
||||
// and replaces username with this result.
|
||||
// returns error if the regexp doesn't compile.
|
||||
func extractNick(search, extract, username, text string) (string, string, error) {
|
||||
re, err := regexp.Compile(search)
|
||||
if err != nil {
|
||||
return username, text, err
|
||||
}
|
||||
if re.MatchString(username) {
|
||||
re, err = regexp.Compile(extract)
|
||||
if err != nil {
|
||||
return username, text, err
|
||||
}
|
||||
res := re.FindAllStringSubmatch(text, 1)
|
||||
// only replace if we have exactly 1 match
|
||||
if len(res) > 0 && len(res[0]) == 2 {
|
||||
username = res[0][1]
|
||||
text = strings.Replace(text, res[0][0], "", 1)
|
||||
}
|
||||
}
|
||||
return username, text, nil
|
||||
}
|
||||
75
gateway/handlers_test.go
Normal file
75
gateway/handlers_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIgnoreEvent(t *testing.T) {
|
||||
eventTests := map[string]struct {
|
||||
input string
|
||||
dest *bridge.Bridge
|
||||
output bool
|
||||
}{
|
||||
"avatar mattermost": {
|
||||
input: config.EventAvatarDownload,
|
||||
dest: &bridge.Bridge{Protocol: "mattermost"},
|
||||
output: false,
|
||||
},
|
||||
"avatar slack": {
|
||||
input: config.EventAvatarDownload,
|
||||
dest: &bridge.Bridge{Protocol: "slack"},
|
||||
output: true,
|
||||
},
|
||||
"avatar telegram": {
|
||||
input: config.EventAvatarDownload,
|
||||
dest: &bridge.Bridge{Protocol: "telegram"},
|
||||
output: false,
|
||||
},
|
||||
}
|
||||
gw := &Gateway{}
|
||||
for testname, testcase := range eventTests {
|
||||
output := gw.ignoreEvent(testcase.input, testcase.dest)
|
||||
assert.Equalf(t, testcase.output, output, "case '%s' failed", testname)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExtractNick(t *testing.T) {
|
||||
eventTests := map[string]struct {
|
||||
search string
|
||||
extract string
|
||||
username string
|
||||
text string
|
||||
resultUsername string
|
||||
resultText string
|
||||
}{
|
||||
"test1": {
|
||||
search: "fromgitter",
|
||||
extract: "<(.*?)>\\s+",
|
||||
username: "fromgitter",
|
||||
text: "<userx> blahblah",
|
||||
resultUsername: "userx",
|
||||
resultText: "blahblah",
|
||||
},
|
||||
"test2": {
|
||||
search: "<.*?bot>",
|
||||
//extract: `\((.*?)\)\s+`,
|
||||
extract: "\\((.*?)\\)\\s+",
|
||||
username: "<matterbot>",
|
||||
text: "(userx) blahblah (abc) test",
|
||||
resultUsername: "userx",
|
||||
resultText: "blahblah (abc) test",
|
||||
},
|
||||
}
|
||||
// gw := &Gateway{}
|
||||
for testname, testcase := range eventTests {
|
||||
resultUsername, resultText, _ := extractNick(testcase.search, testcase.extract, testcase.username, testcase.text)
|
||||
assert.Equalf(t, testcase.resultUsername, resultUsername, "case '%s' failed", testname)
|
||||
assert.Equalf(t, testcase.resultText, resultText, "case '%s' failed", testname)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,32 +2,45 @@ package gateway
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
samechannelgateway "github.com/42wim/matterbridge/gateway/samechannel"
|
||||
"github.com/42wim/matterbridge/gateway/samechannel"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
config.Config
|
||||
sync.RWMutex
|
||||
|
||||
BridgeMap map[string]bridge.Factory
|
||||
Gateways map[string]*Gateway
|
||||
Message chan config.Message
|
||||
MattermostPlugin chan config.Message
|
||||
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func NewRouter(cfg config.Config) (*Router, error) {
|
||||
// NewRouter initializes a new Matterbridge router for the specified configuration and
|
||||
// sets up all required gateways.
|
||||
func NewRouter(rootLogger *logrus.Logger, cfg config.Config, bridgeMap map[string]bridge.Factory) (*Router, error) {
|
||||
logger := rootLogger.WithFields(logrus.Fields{"prefix": "router"})
|
||||
|
||||
r := &Router{
|
||||
Config: cfg,
|
||||
BridgeMap: bridgeMap,
|
||||
Message: make(chan config.Message),
|
||||
MattermostPlugin: make(chan config.Message),
|
||||
Gateways: make(map[string]*Gateway),
|
||||
logger: logger,
|
||||
}
|
||||
sgw := samechannelgateway.New(cfg)
|
||||
gwconfigs := sgw.GetConfig()
|
||||
sgw := samechannel.New(cfg)
|
||||
gwconfigs := append(sgw.GetConfig(), cfg.BridgeValues().Gateway...)
|
||||
|
||||
for _, entry := range append(gwconfigs, cfg.BridgeValues().Gateway...) {
|
||||
for idx := range gwconfigs {
|
||||
entry := &gwconfigs[idx]
|
||||
if !entry.Enable {
|
||||
continue
|
||||
}
|
||||
@@ -37,34 +50,66 @@ func NewRouter(cfg config.Config) (*Router, error) {
|
||||
if _, ok := r.Gateways[entry.Name]; ok {
|
||||
return nil, fmt.Errorf("Gateway with name %s already exists", entry.Name)
|
||||
}
|
||||
r.Gateways[entry.Name] = New(entry, r)
|
||||
r.Gateways[entry.Name] = New(rootLogger, entry, r)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Start will connect all gateways belonging to this router and subsequently route messages
|
||||
// between them.
|
||||
func (r *Router) Start() error {
|
||||
m := make(map[string]*bridge.Bridge)
|
||||
for _, gw := range r.Gateways {
|
||||
flog.Infof("Parsing gateway %s", gw.Name)
|
||||
r.logger.Infof("Parsing gateway %s", gw.Name)
|
||||
for _, br := range gw.Bridges {
|
||||
m[br.Account] = br
|
||||
}
|
||||
}
|
||||
for _, br := range m {
|
||||
flog.Infof("Starting bridge: %s ", br.Account)
|
||||
r.logger.Infof("Starting bridge: %s ", br.Account)
|
||||
err := br.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err)
|
||||
e := fmt.Errorf("Bridge %s failed to start: %v", br.Account, err)
|
||||
if r.disableBridge(br, e) {
|
||||
continue
|
||||
}
|
||||
return e
|
||||
}
|
||||
err = br.JoinChannels()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Bridge %s failed to join channel: %v", br.Account, err)
|
||||
e := fmt.Errorf("Bridge %s failed to join channel: %v", br.Account, err)
|
||||
if r.disableBridge(br, e) {
|
||||
continue
|
||||
}
|
||||
return e
|
||||
}
|
||||
}
|
||||
// remove unused bridges
|
||||
for _, gw := range r.Gateways {
|
||||
for i, br := range gw.Bridges {
|
||||
if br.Bridger == nil {
|
||||
r.logger.Errorf("removing failed bridge %s", i)
|
||||
delete(gw.Bridges, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
go r.handleReceive()
|
||||
//go r.updateChannelMembers()
|
||||
return nil
|
||||
}
|
||||
|
||||
// disableBridge returns true and empties a bridge if we have IgnoreFailureOnStart configured
|
||||
// otherwise returns false
|
||||
func (r *Router) disableBridge(br *bridge.Bridge, err error) bool {
|
||||
if r.BridgeValues().General.IgnoreFailureOnStart {
|
||||
r.logger.Error(err)
|
||||
// setting this bridge empty
|
||||
*br = bridge.Bridge{}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *Router) getBridge(account string) *bridge.Bridge {
|
||||
for _, gw := range r.Gateways {
|
||||
if br, ok := gw.Bridges[account]; ok {
|
||||
@@ -77,42 +122,61 @@ func (r *Router) getBridge(account string) *bridge.Bridge {
|
||||
func (r *Router) handleReceive() {
|
||||
for msg := range r.Message {
|
||||
msg := msg // scopelint
|
||||
if msg.Event == config.EventFailure {
|
||||
Loop:
|
||||
for _, gw := range r.Gateways {
|
||||
for _, br := range gw.Bridges {
|
||||
if msg.Account == br.Account {
|
||||
go gw.reconnectBridge(br)
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if msg.Event == config.EventRejoinChannels {
|
||||
for _, gw := range r.Gateways {
|
||||
for _, br := range gw.Bridges {
|
||||
if msg.Account == br.Account {
|
||||
br.Joined = make(map[string]bool)
|
||||
br.JoinChannels()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
r.handleEventGetChannelMembers(&msg)
|
||||
r.handleEventFailure(&msg)
|
||||
r.handleEventRejoinChannels(&msg)
|
||||
|
||||
filesHandled := false
|
||||
for _, gw := range r.Gateways {
|
||||
// record all the message ID's of the different bridges
|
||||
var msgIDs []*BrMsgID
|
||||
if !gw.ignoreMessage(&msg) {
|
||||
msg.Timestamp = time.Now()
|
||||
gw.modifyMessage(&msg)
|
||||
if gw.ignoreMessage(&msg) {
|
||||
continue
|
||||
}
|
||||
msg.Timestamp = time.Now()
|
||||
gw.modifyMessage(&msg)
|
||||
if !filesHandled {
|
||||
gw.handleFiles(&msg)
|
||||
for _, br := range gw.Bridges {
|
||||
msgIDs = append(msgIDs, gw.handleMessage(msg, br)...)
|
||||
}
|
||||
// only add the message ID if it doesn't already exists
|
||||
if _, ok := gw.Messages.Get(msg.Protocol + " " + msg.ID); !ok && msg.ID != "" {
|
||||
filesHandled = true
|
||||
}
|
||||
for _, br := range gw.Bridges {
|
||||
msgIDs = append(msgIDs, gw.handleMessage(&msg, br)...)
|
||||
}
|
||||
|
||||
if msg.ID != "" {
|
||||
_, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateChannelMembers sends every minute an GetChannelMembers event to all bridges.
|
||||
func (r *Router) updateChannelMembers() {
|
||||
// TODO sleep a minute because slack can take a while
|
||||
// fix this by having actually connectionDone events send to the router
|
||||
time.Sleep(time.Minute)
|
||||
for {
|
||||
for _, gw := range r.Gateways {
|
||||
for _, br := range gw.Bridges {
|
||||
// only for slack now
|
||||
if br.Protocol != "slack" {
|
||||
continue
|
||||
}
|
||||
r.logger.Debugf("sending %s to %s", config.EventGetChannelMembers, br.Account)
|
||||
if _, err := br.Send(config.Message{Event: config.EventGetChannelMembers}); err != nil {
|
||||
r.logger.Errorf("updateChannelMembers: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package samechannelgateway
|
||||
package samechannel
|
||||
|
||||
import (
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package samechannelgateway
|
||||
package samechannel
|
||||
|
||||
import (
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const testConfig = `
|
||||
@@ -66,7 +68,9 @@ var (
|
||||
)
|
||||
|
||||
func TestGetConfig(t *testing.T) {
|
||||
cfg := config.NewConfigFromString([]byte(testConfig))
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(ioutil.Discard)
|
||||
cfg := config.NewConfigFromString(logger, []byte(testConfig))
|
||||
sgw := New(cfg)
|
||||
configs := sgw.GetConfig()
|
||||
assert.Equal(t, []config.Gateway{expectedConfig}, configs)
|
||||
|
||||
91
go.mod
91
go.mod
@@ -2,79 +2,68 @@ module github.com/42wim/matterbridge
|
||||
|
||||
require (
|
||||
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
|
||||
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 // indirect
|
||||
github.com/Philipp15b/go-steam v0.0.0-20161020161927-e0f3bb9566e3
|
||||
github.com/alecthomas/log4go v0.0.0-20160307011253-e5dc62318d9b // indirect
|
||||
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
|
||||
github.com/Jeffail/gabs v1.1.1 // indirect
|
||||
github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329
|
||||
github.com/Rhymen/go-whatsapp v0.0.2
|
||||
github.com/bwmarrin/discordgo v0.19.0
|
||||
github.com/dfordsoft/golib v0.0.0-20180313113957-2ea3495aee1d
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20170508165458-6c8dedd55f8a // indirect
|
||||
// github.com/bwmarrin/discordgo v0.19.0
|
||||
github.com/d5/tengo v1.24.1
|
||||
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180428185002-212b1541150c
|
||||
github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc // indirect
|
||||
github.com/google/gops v0.0.0-20170319002943-62f833fc9f6c
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible
|
||||
github.com/google/gops v0.3.6
|
||||
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect
|
||||
github.com/gorilla/schema v0.0.0-20170317173100-f3c80893412c
|
||||
github.com/gorilla/schema v1.1.0
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad
|
||||
github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.1
|
||||
github.com/hpcloud/tail v1.0.0 // indirect
|
||||
github.com/jpillora/backoff v0.0.0-20170222002228-06c7a16c845d
|
||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7
|
||||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||
github.com/kardianos/osext v0.0.0-20170207191655-9b883c5eb462 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/labstack/echo v0.0.0-20180219162101-7eec915044a1
|
||||
github.com/labstack/gommon v0.2.1 // indirect
|
||||
github.com/lrstanley/girc v0.0.0-20180913221000-0fb5b684054e
|
||||
github.com/labstack/echo/v4 v4.1.6
|
||||
github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398
|
||||
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
|
||||
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect
|
||||
github.com/magiconair/properties v0.0.0-20180217134545-2c9e95027885 // indirect
|
||||
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d
|
||||
github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91
|
||||
github.com/matterbridge/gomatrix v0.0.0-20171224233421-78ac6a1a0f5f
|
||||
github.com/matterbridge/gozulipbot v0.0.0-20180507190239-b6bb12d33544
|
||||
github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea
|
||||
github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18
|
||||
github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61
|
||||
github.com/mattermost/platform v4.6.2+incompatible
|
||||
github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597 // indirect
|
||||
github.com/mattn/go-isatty v0.0.0-20170216235908-dda3de49cbfc // indirect
|
||||
github.com/mattermost/mattermost-server v5.5.0+incompatible
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 // indirect
|
||||
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect
|
||||
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect
|
||||
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9
|
||||
github.com/nicksnyder/go-i18n v1.4.0 // indirect
|
||||
github.com/nlopes/slack v0.4.0
|
||||
github.com/nlopes/slack v0.5.0
|
||||
github.com/onsi/ginkgo v1.6.0 // indirect
|
||||
github.com/onsi/gomega v1.4.1 // indirect
|
||||
github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83
|
||||
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c
|
||||
github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect
|
||||
github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e // indirect
|
||||
github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271
|
||||
github.com/pkg/errors v0.8.0 // indirect
|
||||
github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a
|
||||
github.com/russross/blackfriday v2.0.0+incompatible
|
||||
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320
|
||||
github.com/rs/xid v1.2.1
|
||||
github.com/russross/blackfriday v1.5.2
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca
|
||||
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 // indirect
|
||||
github.com/shazow/ssh-chat v0.0.0-20171012174035-2078e1381991
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
|
||||
github.com/sirupsen/logrus v1.2.0
|
||||
github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
|
||||
github.com/spf13/afero v0.0.0-20180211162714-bbf41cb36dff // indirect
|
||||
github.com/spf13/cast v1.2.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec // indirect
|
||||
github.com/spf13/pflag v0.0.0-20180220143236-ee5fd03fd6ac // indirect
|
||||
github.com/spf13/viper v0.0.0-20171227194143-aafc9e6bc7b7
|
||||
github.com/stretchr/testify v1.2.2
|
||||
github.com/spf13/viper v1.4.0
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a // indirect
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
|
||||
github.com/zfjagann/golang-ring v0.0.0-20141111230621-17637388c9f6
|
||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect
|
||||
golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 // indirect
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
|
||||
golang.org/x/sys v0.0.0-20181116161606-93218def8b18 // indirect
|
||||
golang.org/x/text v0.0.0-20180511172408-5c1cf69b5978 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2
|
||||
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a // indirect
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179 // indirect
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f
|
||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2 // indirect
|
||||
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe // indirect
|
||||
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 // indirect
|
||||
golang.org/x/image v0.0.0-20190616094056-33659d3de4f5
|
||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129 // indirect
|
||||
)
|
||||
|
||||
replace github.com/bwmarrin/discordgo v0.19.0 => github.com/MOZGIII/discordgo v0.19.1-0.20190812115637-1e74183814f9
|
||||
|
||||
357
go.sum
357
go.sum
@@ -1,167 +1,332 @@
|
||||
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/go.mod h1:jL0YSXMs/txjtGJ4PWrmETOk6KUHMDPMshgQZlTeB3Y=
|
||||
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 h1:v/zr4ns/4sSahF9KBm4Uc933bLsEEv7LuT63CJ019yo=
|
||||
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Philipp15b/go-steam v0.0.0-20161020161927-e0f3bb9566e3 h1:V4+1E1SRYUySqwOoI3ZphFADtabbF568zTHa5ix/zU0=
|
||||
github.com/Philipp15b/go-steam v0.0.0-20161020161927-e0f3bb9566e3/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg=
|
||||
github.com/alecthomas/log4go v0.0.0-20160307011253-e5dc62318d9b h1:1OpGXps6UOY5HtQaQcLowsV1qMWCNBzhFvK7q4fgXtc=
|
||||
github.com/alecthomas/log4go v0.0.0-20160307011253-e5dc62318d9b/go.mod h1:iCVmQ9g4TfaRX5m5jq5sXY7RXYWPv9/PynM/GocbG3w=
|
||||
github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY=
|
||||
github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q=
|
||||
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII=
|
||||
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
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/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
|
||||
github.com/MOZGIII/discordgo v0.19.1-0.20190812115637-1e74183814f9 h1:2AlsZSdWfhhuyzNgRejZDkkLq1cAA2K8TaMoadSXeJE=
|
||||
github.com/MOZGIII/discordgo v0.19.1-0.20190812115637-1e74183814f9/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q=
|
||||
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/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg=
|
||||
github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA=
|
||||
github.com/Rhymen/go-whatsapp v0.0.2 h1:MelwdquHuuNObBGV7CpXbky2aVdilx/CwiXMwZvS74U=
|
||||
github.com/Rhymen/go-whatsapp v0.0.2/go.mod h1:qf/2PQi82Okxw/igghu/oMGzTeUYuKBq1JNo3tdQyNg=
|
||||
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/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/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/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/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dfordsoft/golib v0.0.0-20180313113957-2ea3495aee1d h1:rONNnZDE5CYuaSFQk+gP4GEQTXEUcyQ5p6p/dgxIHas=
|
||||
github.com/dfordsoft/golib v0.0.0-20180313113957-2ea3495aee1d/go.mod h1:UGa5M2Sz/Uh13AMse4+RELKCDw7kqgqlTjeGae+7vUY=
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20170508165458-6c8dedd55f8a h1:MuHMeSsXbNEeUyxjB7T9P8s1+5k8OLTC/M27qsVwixM=
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20170508165458-6c8dedd55f8a/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec h1:JEUiu7P9smN7zgX87a2zVnnbPPickIM9Gf9OIhsIgWQ=
|
||||
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec/go.mod h1:UGa5M2Sz/Uh13AMse4+RELKCDw7kqgqlTjeGae+7vUY=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/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/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180428185002-212b1541150c h1:3gMh737vMGqAkkkSfNbwjO8VRHOSaCjYRG4y9xVMEIQ=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180428185002-212b1541150c/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc h1:wdhDSKrkYy24mcfzuA3oYm58h0QkyXjwERCkzJDP5kA=
|
||||
github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/gops v0.0.0-20170319002943-62f833fc9f6c h1:MrMA1vhRTNidtgENqmsmLOIUS6ixMBOU/g10rm7IUe8=
|
||||
github.com/google/gops v0.0.0-20170319002943-62f833fc9f6c/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0=
|
||||
github.com/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/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
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.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
|
||||
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/go.mod h1:lEO7XoHJ/xNRBCxrn4h/CEB67h0kW1B0t4ooP2yrjUA=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f h1:FDM3EtwZLyhW48YRiyqjivNlNZjAObv4xt4NnJaU+NQ=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/schema v0.0.0-20170317173100-f3c80893412c h1:mORYpib1aLu3M2Oi50Z1pNTXuDJEHcoLb6oo6VdOutk=
|
||||
github.com/gorilla/schema v0.0.0-20170317173100-f3c80893412c/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
|
||||
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po=
|
||||
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb h1:1OvvPvZkn/yCQ3xBcM8y4020wdkMXPHLB4+NfoGWh4U=
|
||||
github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
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/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jpillora/backoff v0.0.0-20170222002228-06c7a16c845d h1:ETeT81zgLgSNc4BWdDO2Fg9ekVItYErbNtE8mKD2pJA=
|
||||
github.com/jpillora/backoff v0.0.0-20170222002228-06c7a16c845d/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||
github.com/jessevdk/go-flags v1.3.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/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/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kardianos/osext v0.0.0-20170207191655-9b883c5eb462 h1:oSOOTPHkCzMeu1vJ0nHxg5+XZBdMMjNa+6NPnm8arok=
|
||||
github.com/kardianos/osext v0.0.0-20170207191655-9b883c5eb462/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/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/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/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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labstack/echo v0.0.0-20180219162101-7eec915044a1 h1:cOIt0LZKdfeirAfTP4VtIJuWbjVTGtd1suuPXp/J+dE=
|
||||
github.com/labstack/echo v0.0.0-20180219162101-7eec915044a1/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/gommon v0.2.1 h1:C+I4NYknueQncqKYZQ34kHsLZJVeB5KwPUhnO0nmbpU=
|
||||
github.com/labstack/gommon v0.2.1/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/lrstanley/girc v0.0.0-20180913221000-0fb5b684054e h1:RpktB2igr6nS1EN7bCvjldAEfngrM5GyAbmOa4/cafU=
|
||||
github.com/lrstanley/girc v0.0.0-20180913221000-0fb5b684054e/go.mod h1:7cRs1SIBfKQ7e3Tam6GKTILSNHzR862JD0JpINaZoJk=
|
||||
github.com/labstack/echo/v4 v4.1.6 h1:WOvLa4T1KzWCRpANwz0HGgWDelXSSGwIKtKBbFdHTv4=
|
||||
github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE=
|
||||
github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
|
||||
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/go.mod h1:7cRs1SIBfKQ7e3Tam6GKTILSNHzR862JD0JpINaZoJk=
|
||||
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU=
|
||||
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0=
|
||||
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns=
|
||||
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg=
|
||||
github.com/magiconair/properties v0.0.0-20180217134545-2c9e95027885 h1:HWxJJvF+QceKcql4r9PC93NtMEgEBfBxlQrZPvbcQvs=
|
||||
github.com/magiconair/properties v0.0.0-20180217134545-2c9e95027885/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d h1:F+Sr+C0ojSlYQ37BLylQtSFmyQULe3jbAygcyXQ9mVs=
|
||||
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d/go.mod h1:c6MxwqHD+0HvtAJjsHMIdPCiAwGiQwPRPTp69ACMg8A=
|
||||
github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91 h1:KzDEcy8eDbTx881giW8a6llsAck3e2bJvMyKvh1IK+k=
|
||||
github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91/go.mod h1:ECDRehsR9TYTKCAsRS8/wLeOk6UUqDydw47ln7wG41Q=
|
||||
github.com/matterbridge/gomatrix v0.0.0-20171224233421-78ac6a1a0f5f h1:2eKh6Qi/sJ8bXvYMoyVfQxHgR8UcCDWjOmhV1oCstMU=
|
||||
github.com/matterbridge/gomatrix v0.0.0-20171224233421-78ac6a1a0f5f/go.mod h1:+jWeaaUtXQbBRdKYWfjW6JDDYiI2XXE+3NnTjW5kg8g=
|
||||
github.com/matterbridge/gozulipbot v0.0.0-20180507190239-b6bb12d33544 h1:A8lLG3DAu75B5jITHs9z4JBmU6oCq1WiUNnDAmqKCZc=
|
||||
github.com/matterbridge/gozulipbot v0.0.0-20180507190239-b6bb12d33544/go.mod h1:yAjnZ34DuDyPHMPHHjOsTk/FefW4JJjoMMCGt/8uuQA=
|
||||
github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea h1:kaADGqpK4gGO2BpzEyJrBxq2Jc57Rsar4i2EUxcACUc=
|
||||
github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea/go.mod h1:+jWeaaUtXQbBRdKYWfjW6JDDYiI2XXE+3NnTjW5kg8g=
|
||||
github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18 h1:fLhwXtWGtfTgZVxHG1lcKjv+re7dRwyyuYFNu69xdho=
|
||||
github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18/go.mod h1:yAjnZ34DuDyPHMPHHjOsTk/FefW4JJjoMMCGt/8uuQA=
|
||||
github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61 h1:R/MgM/eUyRBQx2FiH6JVmXck8PaAuKfe2M1tWIzW7nE=
|
||||
github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61/go.mod h1:iXGEotOvwI1R1SjLxRc+BF5rUORTMtE0iMZBT2lxqAU=
|
||||
github.com/mattermost/platform v4.6.2+incompatible h1:9WqKNuJFIp6SDYn5wl1RF5urdhEw8d7o5tAOwT1MW0A=
|
||||
github.com/mattermost/platform v4.6.2+incompatible/go.mod h1:HjGKtkQNu3HXTOykPMQckMnH11WHvNvQqDBNnVXVbfM=
|
||||
github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597 h1:hGizH4aMDFFt1iOA4HNKC13lqIBoCyxIjWcAnWIy7aU=
|
||||
github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.0-20170216235908-dda3de49cbfc h1:pK7tzC30erKOTfEDCYGvPZQCkmM9X5iSmmAR5m9x3Yc=
|
||||
github.com/mattn/go-isatty v0.0.0-20170216235908-dda3de49cbfc/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattermost/mattermost-server v5.5.0+incompatible h1:0wcLGgYtd+YImtLDPf2AOfpBHxbU4suATx+6XKw1XbU=
|
||||
github.com/mattermost/mattermost-server v5.5.0+incompatible/go.mod h1:5L6MjAec+XXQwMIt791Ganu45GKsSiM+I0tLR9wUj8Y=
|
||||
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
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/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 h1:oKIteTqeSpenyTrOVj5zkiyCaflLa8B+CD0324otT+o=
|
||||
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
|
||||
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff h1:HLGD5/9UxxfEuO9DtP8gnTmNtMxbPyhYltfxsITel8g=
|
||||
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E=
|
||||
github.com/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/go.mod h1:A5SRAcpTemjGgIuBq6Kic2yHcoeUFWUinOAlMP/i9xo=
|
||||
github.com/nicksnyder/go-i18n v1.4.0 h1:AgLl+Yq7kg5OYlzCgu9cKTZOyI4tD/NgukKqLqC8E+I=
|
||||
github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
|
||||
github.com/nlopes/slack v0.4.0 h1:OVnHm7lv5gGT5gkcHsZAyw++oHVFihbjWbL3UceUpiA=
|
||||
github.com/nlopes/slack v0.4.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
|
||||
github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0=
|
||||
github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
|
||||
github.com/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/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
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/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83 h1:XQonH5Iv5rbyIkMJOQ4xKmKHQTh8viXtRSmep5Ca5I4=
|
||||
github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83/go.mod h1:YnNlZP7l4MhyGQ4CBRwv6ohZTPrUJJZtEv4ZgADkbs4=
|
||||
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c h1:P6XGcuPTigoHf4TSu+3D/7QOQ1MbL6alNwrGhcW7sKw=
|
||||
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/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
|
||||
github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e h1:ZW8599OjioQsmBbkGpyruHUlRVQceYFWnJsGr4NCkiA=
|
||||
github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271 h1:wQ9lVx75za6AT2kI0S9QID0uWuwTWnvcTfN+uw1F8vg=
|
||||
github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271/go.mod h1:G7LufuPajuIvdt9OitkNt2qh0mmvD4bfRgRM7bhDIOA=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320 h1:YxcQy/DV+48NGv1lxx1vsWBzs6W1f1ogubkuCozxpX0=
|
||||
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320/go.mod h1:G7LufuPajuIvdt9OitkNt2qh0mmvD4bfRgRM7bhDIOA=
|
||||
github.com/pkg/errors v0.8.0/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a h1:UWKek6MK3K6/TpbsFcv+8rrO6rSc6KKSp2FbMOHWsq4=
|
||||
github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
|
||||
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/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/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 h1:Lx3BlDGFElJt4u/zKc9A3BuGYbQAGlEFyPuUA3jeMD0=
|
||||
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI=
|
||||
github.com/shazow/ssh-chat v0.0.0-20171012174035-2078e1381991 h1:PQiUTDzUC5EUh0vNurK7KQS22zlKqLLOFn+K9nJXDQQ=
|
||||
github.com/shazow/ssh-chat v0.0.0-20171012174035-2078e1381991/go.mod h1:KwtnpMClmrXsHCKTbRui5xBUNt17n1GGrGhdiw2KcoY=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||
github.com/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/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7 h1:80VN+vGkqM773Br/uNNTSheo3KatTgV8IpjIKjvVLng=
|
||||
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
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/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 h1:lXQ+j+KwZcbwrbgU0Rp4Eglg3EJLHbuZU3BbOqAGBmg=
|
||||
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo=
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/spf13/afero v0.0.0-20180211162714-bbf41cb36dff h1:HLvGWId7M56TfuxTeZ6aoiTAcrWO5Mnq/ArwVRgV62I=
|
||||
github.com/spf13/afero v0.0.0-20180211162714-bbf41cb36dff/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v0.0.0-20180220143236-ee5fd03fd6ac h1:+uzyQ0TQ3aKorQxsOjcDDgE7CuUXwpkKnK19LULQALQ=
|
||||
github.com/spf13/pflag v0.0.0-20180220143236-ee5fd03fd6ac/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v0.0.0-20171227194143-aafc9e6bc7b7 h1:Wj4cg2M6Um7j1N7yD/mxsdy1/wrsdjzVha2eWdOhti8=
|
||||
github.com/spf13/viper v0.0.0-20171227194143-aafc9e6bc7b7/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/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/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
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.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.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a h1:AOcehBWpFhYPYw0ioDTppQzgI8pAAahVCiMSKTp9rbo=
|
||||
github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
||||
github.com/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/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
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/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
github.com/zfjagann/golang-ring v0.0.0-20141111230621-17637388c9f6 h1:/WULP+6asFz569UbOwg87f3iDT7T+GF5/vjLmL51Pdk=
|
||||
github.com/zfjagann/golang-ring v0.0.0-20141111230621-17637388c9f6/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU=
|
||||
github.com/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/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2 h1:UQwvu7FjUEdVYofx0U6bsc5odNE7wa5TSA0fl559GcA=
|
||||
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/go.mod h1:JT4uoTz0tfPoyVH88GZoWDNm5NHJI2VbUW+eyPClueI=
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179 h1:rbON2KwBnWuFMlSHM8LELLlwroDRZw6xv0e6il6e5dk=
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8=
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f h1:jwXy/CsM4xS2aoiF2fHAlukmInWhd2TlWB+HDCyvzKc=
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f/go.mod h1:SIHlEr9462fpIfTrVWf3GqQDxnA65Vm3BMMsUtuA6W0=
|
||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2 h1:wD/sPUgx2QJFPTyXZpJnLaROolfeKuruh06U4pRV0WY=
|
||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2/go.mod h1:wQk4rLkWrdOPjUAtqJRJ10hIlseLSVYWP95PLrjDF9s=
|
||||
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe h1:5kUPFAF52umOUPH12MuNUmyVTseJRNBftDl/KfsvX3I=
|
||||
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe/go.mod h1:P9LSM1KVzrIstFgUaveuwiAm8PK5VTB3yJEU8kqlbrU=
|
||||
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 h1:uPZaMiz6Sz0PZs3IZJWpU5qHKGNy///1pacZC9txiUI=
|
||||
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638/go.mod h1:EGRJaqe2eO9XGmFtQCvV3Lm9NLico3UhFwUpCG/+mVU=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
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/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||
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-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
|
||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 h1:BkNcmLtAVeWe9h5k0jt24CQgaG5vb4x/doFbAiEC/Ho=
|
||||
golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/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/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-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-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-20181116161606-93218def8b18 h1:Wh+XCfg3kNpjhdq2LXrsiOProjtQZKme5XUx7VcxwAw=
|
||||
golang.org/x/sys v0.0.0-20181116161606-93218def8b18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.0.0-20180511172408-5c1cf69b5978 h1:WNm0tmiuBMW4FJRuXKWOqaQfmKptHs0n8nTCyG0ayjc=
|
||||
golang.org/x/text v0.0.0-20180511172408-5c1cf69b5978/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-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/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 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/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/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129 h1:RBgb9aPUbZ9nu66ecQNIBNsA7j3mB5h8PNDIfhPjaJg=
|
||||
gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/goversion v1.0.0 h1:/IhXBiai89TyuerPquiZZ39IQkTfAUbZB2awsyYZ/2c=
|
||||
rsc.io/goversion v1.0.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
|
||||
@@ -38,7 +38,7 @@ type Config struct {
|
||||
func New(url string, config Config) *Client {
|
||||
c := &Client{In: make(chan Message), Config: config}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}, //nolint:gosec
|
||||
}
|
||||
c.httpclient = &http.Client{Transport: tr}
|
||||
_, _, err := net.SplitHostPort(c.BindAddress)
|
||||
|
||||
288
internal/bindata.go
Normal file
288
internal/bindata.go
Normal 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, "/")...)...)
|
||||
}
|
||||
19
internal/tengo/outmessage.tengo
Normal file
19
internal/tengo/outmessage.tengo
Normal 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
|
||||
@@ -8,51 +8,78 @@ import (
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/gateway"
|
||||
"github.com/42wim/matterbridge/gateway/bridgemap"
|
||||
"github.com/google/gops/agent"
|
||||
prefixed "github.com/matterbridge/logrus-prefixed-formatter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "1.12.1-dev"
|
||||
version = "1.15.2-dev"
|
||||
githash string
|
||||
|
||||
flagConfig = flag.String("conf", "matterbridge.toml", "config file")
|
||||
flagDebug = flag.Bool("debug", false, "enable debug")
|
||||
flagVersion = flag.Bool("version", false, "show version")
|
||||
flagGops = flag.Bool("gops", false, "enable gops agent")
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: true})
|
||||
flog := log.WithFields(log.Fields{"prefix": "main"})
|
||||
flagConfig := flag.String("conf", "matterbridge.toml", "config file")
|
||||
flagDebug := flag.Bool("debug", false, "enable debug")
|
||||
flagVersion := flag.Bool("version", false, "show version")
|
||||
flagGops := flag.Bool("gops", false, "enable gops agent")
|
||||
flag.Parse()
|
||||
if *flagGops {
|
||||
agent.Listen(&agent.Options{})
|
||||
defer agent.Close()
|
||||
}
|
||||
if *flagVersion {
|
||||
fmt.Printf("version: %s %s\n", version, githash)
|
||||
return
|
||||
}
|
||||
if *flagDebug || os.Getenv("DEBUG") == "1" {
|
||||
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false, ForceFormatting: true})
|
||||
flog.Info("Enabling debug")
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
rootLogger := setupLogger()
|
||||
logger := rootLogger.WithFields(logrus.Fields{"prefix": "main"})
|
||||
|
||||
if *flagGops {
|
||||
if err := agent.Listen(agent.Options{}); err != nil {
|
||||
logger.Errorf("Failed to start gops agent: %#v", err)
|
||||
} else {
|
||||
defer agent.Close()
|
||||
}
|
||||
}
|
||||
flog.Printf("Running version %s %s", version, githash)
|
||||
|
||||
logger.Printf("Running version %s %s", version, githash)
|
||||
if strings.Contains(version, "-dev") {
|
||||
flog.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
|
||||
logger.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
|
||||
}
|
||||
cfg := config.NewConfig(*flagConfig)
|
||||
|
||||
cfg := config.NewConfig(rootLogger, *flagConfig)
|
||||
cfg.BridgeValues().General.Debug = *flagDebug
|
||||
r, err := gateway.NewRouter(cfg)
|
||||
|
||||
r, err := gateway.NewRouter(rootLogger, cfg, bridgemap.FullMap)
|
||||
if err != nil {
|
||||
flog.Fatalf("Starting gateway failed: %s", err)
|
||||
logger.Fatalf("Starting gateway failed: %s", err)
|
||||
}
|
||||
err = r.Start()
|
||||
if err != nil {
|
||||
flog.Fatalf("Starting gateway failed: %s", err)
|
||||
if err = r.Start(); err != nil {
|
||||
logger.Fatalf("Starting gateway failed: %s", err)
|
||||
}
|
||||
flog.Printf("Gateway(s) started succesfully. Now relaying messages")
|
||||
logger.Printf("Gateway(s) started succesfully. Now relaying messages")
|
||||
select {}
|
||||
}
|
||||
|
||||
func setupLogger() *logrus.Logger {
|
||||
logger := &logrus.Logger{
|
||||
Out: os.Stdout,
|
||||
Formatter: &prefixed.TextFormatter{
|
||||
PrefixPadding: 13,
|
||||
DisableColors: true,
|
||||
FullTimestamp: true,
|
||||
},
|
||||
Level: logrus.InfoLevel,
|
||||
}
|
||||
if *flagDebug || os.Getenv("DEBUG") == "1" {
|
||||
logger.Formatter = &prefixed.TextFormatter{
|
||||
PrefixPadding: 13,
|
||||
DisableColors: true,
|
||||
FullTimestamp: false,
|
||||
ForceFormatting: true,
|
||||
}
|
||||
logger.Level = logrus.DebugLevel
|
||||
logger.WithFields(logrus.Fields{"prefix": "main"}).Info("Enabling debug logging.")
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#This is configuration for matterbridge.
|
||||
#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
|
||||
###################################################################
|
||||
@@ -27,7 +29,7 @@ UseTLS=false
|
||||
#OPTIONAL (default 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
|
||||
#OPTIONAL (default false)
|
||||
SkipTLSVerify=true
|
||||
@@ -96,7 +98,13 @@ RejoinDelay=0
|
||||
#Only works in IRC right now.
|
||||
ColorNicks=false
|
||||
|
||||
#RunCommands allows you to send RAW irc commands after connection
|
||||
#Array of strings
|
||||
#OPTIONAL (default empty)
|
||||
RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"]
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="ircspammer1 ircspammer2"
|
||||
@@ -124,6 +132,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -134,10 +153,15 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
#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
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#OPTIONAL (default false)
|
||||
@@ -191,6 +215,7 @@ SkipTLSVerify=true
|
||||
## Settings below can be reloaded by editing the file
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="ircspammer1 ircspammer2"
|
||||
@@ -218,6 +243,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#OPTIONAL (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -227,87 +263,7 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#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
|
||||
|
||||
###################################################################
|
||||
#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.
|
||||
#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"] ]
|
||||
|
||||
#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
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
@@ -394,6 +350,12 @@ NickFormatter="plain"
|
||||
#OPTIONAL (default 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.
|
||||
#Useful if username overrides for incoming webhooks isn't enabled on the
|
||||
#mattermost server. If you set PrefixMessagesWithNick to true, each message
|
||||
@@ -411,6 +373,7 @@ EditDisable=false
|
||||
EditSuffix=" (edited)"
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="ircspammer1 ircspammer2"
|
||||
@@ -438,6 +401,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -447,7 +421,7 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
@@ -486,6 +460,7 @@ Token="Yourtokenhere"
|
||||
## Settings below can be reloaded by editing the file
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="ircspammer1 ircspammer2"
|
||||
@@ -513,6 +488,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -522,7 +508,7 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
@@ -552,6 +538,10 @@ ShowTopicChange=false
|
||||
#REQUIRED (when not using webhooks)
|
||||
Token="yourslacktoken"
|
||||
|
||||
#Extra slack specific debug info, warning this generates a lot of output.
|
||||
#OPTIONAL (default false)
|
||||
Debug="false"
|
||||
|
||||
#### Settings for webhook matterbridge.
|
||||
#NOT RECOMMENDED TO USE INCOMING/OUTGOING WEBHOOK. USE SLACK API
|
||||
#AND DEDICATED BOT USER WHEN POSSIBLE!
|
||||
@@ -604,6 +594,7 @@ EditSuffix=" (edited)"
|
||||
PrefixMessagesWithNick=false
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="ircspammer1 ircspammer2"
|
||||
@@ -631,6 +622,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -640,7 +642,7 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
@@ -662,6 +664,8 @@ ShowTopicChange=false
|
||||
#Opportunistically preserve threaded replies between Slack channels.
|
||||
#This only works if the parent message is still in the cache.
|
||||
#Cache is flushed between restarts.
|
||||
#Note: Not currently working on gateways with mixed bridges of
|
||||
# both slack and slack-legacy type. Context in issue #624.
|
||||
#OPTIONAL (default false)
|
||||
PreserveThreading=false
|
||||
|
||||
@@ -697,10 +701,14 @@ Server="yourservername"
|
||||
#OPTIONAL (default false)
|
||||
ShowEmbeds=false
|
||||
|
||||
#Shows the username (minus the discriminator) instead of the server nickname
|
||||
#Shows the username instead of the server nickname
|
||||
#OPTIONAL (default false)
|
||||
UseUserName=false
|
||||
|
||||
#Show #xxxx discriminator with UseUserName
|
||||
#OPTIONAL (default false)
|
||||
UseDiscriminator=false
|
||||
|
||||
#Specify WebhookURL. If given, will relay messages using the Webhook, which gives a better look to messages.
|
||||
#This only works if you have one discord channel, if you have multiple discord channels you'll have to specify it in the gateway config
|
||||
#OPTIONAL (default empty)
|
||||
@@ -715,6 +723,7 @@ EditDisable=false
|
||||
EditSuffix=" (edited)"
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="ircspammer1 ircspammer2"
|
||||
@@ -742,6 +751,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -751,7 +771,7 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
@@ -760,11 +780,16 @@ ShowJoinPart=false
|
||||
#OPTIONAL (default false)
|
||||
StripNick=false
|
||||
|
||||
#Enable to show topic changes from other bridges
|
||||
#Enable to show topic/purpose changes from other bridges
|
||||
#Only works hiding/show topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
#Enable to sync topic/purpose changes from other bridges
|
||||
#Only works syncing topic changes from slack bridge for now
|
||||
#OPTIONAL (default false)
|
||||
SyncTopic=false
|
||||
|
||||
###################################################################
|
||||
#telegram section
|
||||
###################################################################
|
||||
@@ -809,6 +834,11 @@ QuoteDisable=false
|
||||
#OPTIONAL (default "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})")
|
||||
QuoteFormat="{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
|
||||
|
||||
#Convert WebP images to PNG before upload.
|
||||
#https://github.com/42wim/matterbridge/issues/398
|
||||
#OPTIONAL (default false)
|
||||
MediaConvertWebPToPNG=false
|
||||
|
||||
#Disable sending of edits to other bridges
|
||||
#OPTIONAL (default false)
|
||||
EditDisable=false
|
||||
@@ -818,6 +848,7 @@ EditDisable=false
|
||||
EditSuffix=" (edited)"
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="spammer1 spammer2"
|
||||
@@ -845,6 +876,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -858,7 +900,7 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
@@ -881,6 +923,22 @@ ShowTopicChange=false
|
||||
#REQUIRED
|
||||
|
||||
[rocketchat.rockme]
|
||||
#The rocketchat hostname. (prefix it with http or https)
|
||||
#REQUIRED (when not using webhooks)
|
||||
Server="https://yourrocketchatserver.domain.com:443"
|
||||
|
||||
#login/pass of your bot.
|
||||
#login needs to be the login with email address! user@domain.com
|
||||
#Use a dedicated user for this and not your own!
|
||||
#REQUIRED (when not using webhooks)
|
||||
Login="yourlogin@domain.com"
|
||||
Password="yourpass"
|
||||
|
||||
#### Settings for webhook matterbridge.
|
||||
#USE DEDICATED BOT USER WHEN POSSIBLE! This allows you to use advanced features like message editing/deleting and uploads
|
||||
#You don't need to configure this, if you have configured the settings
|
||||
#above.
|
||||
|
||||
#Url is your incoming webhook url as specified in rocketchat
|
||||
#Read #https://rocket.chat/docs/administrator-guides/integrations/#how-to-create-a-new-incoming-webhook
|
||||
#See administration - integrations - new integration - incoming webhook
|
||||
@@ -905,6 +963,8 @@ NoTLS=false
|
||||
#OPTIONAL (default false)
|
||||
SkipTLSVerify=true
|
||||
|
||||
#### End settings for webhook matterbridge.
|
||||
|
||||
## RELOADABLE SETTINGS
|
||||
## Settings below can be reloaded by editing the file
|
||||
|
||||
@@ -912,10 +972,13 @@ SkipTLSVerify=true
|
||||
#Useful if username overrides for incoming webhooks isn't enabled on the
|
||||
#rocketchat server. If you set PrefixMessagesWithNick to true, each message
|
||||
#from bridge to rocketchat will by default be prefixed by the RemoteNickFormat setting. i
|
||||
#if you're using login/pass you can better enable because of this bug:
|
||||
#https://github.com/RocketChat/Rocket.Chat/issues/7549
|
||||
#OPTIONAL (default false)
|
||||
PrefixMessagesWithNick=false
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="ircspammer1 ircspammer2"
|
||||
@@ -943,6 +1006,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -952,7 +1026,7 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
@@ -1002,6 +1076,7 @@ NoHomeServerSuffix=false
|
||||
PrefixMessagesWithNick=false
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="spammer1 spammer2"
|
||||
@@ -1029,6 +1104,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -1038,7 +1124,7 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
@@ -1082,6 +1168,7 @@ Authcode="ABCE12"
|
||||
PrefixMessagesWithNick=false
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="spammer1 spammer2"
|
||||
@@ -1109,6 +1196,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -1118,7 +1216,7 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
@@ -1132,10 +1230,45 @@ StripNick=false
|
||||
#OPTIONAL (default false)
|
||||
ShowTopicChange=false
|
||||
|
||||
|
||||
|
||||
###################################################################
|
||||
#zulip section
|
||||
#
|
||||
# WhatsApp
|
||||
#
|
||||
###################################################################
|
||||
|
||||
[whatsapp.bridge]
|
||||
|
||||
# Number you will use as a relay bot. Tip: Get some disposable sim card, don't rely on your own number.
|
||||
Number="+48111222333"
|
||||
|
||||
# First time that you login you will need to scan QR code, then credentials willl be saved in a session file
|
||||
# If you won't set SessionFile then you will need to scan QR code on every restart
|
||||
# optional (by default the session is stored only in memory, till restarting matterbridge)
|
||||
SessionFile="session-48111222333.gob"
|
||||
|
||||
# If your terminal is white we need to invert QR code in order for it to be scanned properly
|
||||
# optional (default false)
|
||||
QrOnWhiteTerminal=true
|
||||
|
||||
# Messages will be seen by other WhatsApp contacts as coming from the bridge. Original nick will be part of the message.
|
||||
RemoteNickFormat="@{NICK}: "
|
||||
|
||||
# extra label that can be used in the RemoteNickFormat
|
||||
# optional (default empty)
|
||||
Label="Organization"
|
||||
|
||||
|
||||
|
||||
###################################################################
|
||||
#
|
||||
# zulip
|
||||
#
|
||||
###################################################################
|
||||
|
||||
[zulip]
|
||||
|
||||
#You can configure multiple servers "[zulip.name]" or "[zulip.name2]"
|
||||
#In this example we use [zulip.streamchat]
|
||||
#REQUIRED
|
||||
@@ -1154,14 +1287,11 @@ Login="yourbot-bot@yourserver.zulipchat.com"
|
||||
#REQUIRED
|
||||
Server="https://yourserver.zulipchat.com"
|
||||
|
||||
#Topic of the messages matterbridge will use
|
||||
#OPTIONAL (default "matterbridge")
|
||||
Topic="matterbridge"
|
||||
|
||||
## RELOADABLE SETTINGS
|
||||
## Settings below can be reloaded by editing the file
|
||||
|
||||
#Nicks you want to ignore.
|
||||
#Regular expressions supported
|
||||
#Messages from those users will not be sent to other bridges.
|
||||
#OPTIONAL
|
||||
IgnoreNicks="spammer1 spammer2"
|
||||
@@ -1189,6 +1319,17 @@ ReplaceMessages=[ ["cat","dog"] ]
|
||||
#optional (default empty)
|
||||
ReplaceNicks=[ ["user--","user"] ]
|
||||
|
||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
||||
#some examples:
|
||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
||||
#you can use multiple entries for multiplebots
|
||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
||||
#OPTIONAL (default empty)
|
||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
||||
|
||||
#extra label that can be used in the RemoteNickFormat
|
||||
#optional (default empty)
|
||||
Label=""
|
||||
@@ -1198,7 +1339,7 @@ Label=""
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
#Enable to show users joins/parts from other bridges
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
||||
#OPTIONAL (default false)
|
||||
ShowJoinPart=false
|
||||
|
||||
@@ -1260,6 +1401,7 @@ RemoteNickFormat="{NICK}"
|
||||
#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 "{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)
|
||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||
|
||||
@@ -1303,6 +1445,73 @@ MediaDownloadSize=1000000
|
||||
#OPTIONAL (default empty)
|
||||
MediaDownloadBlacklist=[".html$",".htm$"]
|
||||
|
||||
#IgnoreFailureOnStart allows you to ignore failing bridges on startup.
|
||||
#Matterbridge will disable the failed bridge and continue with the other ones.
|
||||
#Context: https://github.com/42wim/matterbridge/issues/455
|
||||
#OPTIONAL (default false)
|
||||
IgnoreFailureOnStart=false
|
||||
|
||||
###################################################################
|
||||
#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
|
||||
|
||||
[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.
|
||||
#The script will have the following global variables:
|
||||
#to modify: msgUsername and msgText
|
||||
#to read: msgChannel and msgAccount
|
||||
#
|
||||
#The script is reloaded on every message, so you can modify the script on the fly.
|
||||
#
|
||||
#Example script can be found in https://github.com/42wim/matterbridge/tree/master/gateway/bench.tengo
|
||||
#and https://github.com/42wim/matterbridge/tree/master/contrib/example.tengo
|
||||
#
|
||||
#The example below will check if the text contains blah and if so, it'll replace the text and the username of that message.
|
||||
#text := import("text")
|
||||
#if text.re_match("blah",msgText) {
|
||||
# msgText="replaced by this"
|
||||
# msgUsername="fakeuser"
|
||||
#}
|
||||
#OPTIONAL (default empty)
|
||||
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
|
||||
###################################################################
|
||||
@@ -1324,36 +1533,42 @@ name="gateway1"
|
||||
##OPTIONAL (default false)
|
||||
enable=true
|
||||
|
||||
#[[gateway.in]] specifies the account and channels we will receive messages from.
|
||||
#The following example bridges between mattermost and irc
|
||||
# [[gateway.in]] specifies the account and channels we will receive messages from.
|
||||
# The following example bridges between mattermost and irc
|
||||
[[gateway.in]]
|
||||
|
||||
#account specified above
|
||||
#REQUIRED
|
||||
# account specified above
|
||||
# REQUIRED
|
||||
account="irc.freenode"
|
||||
#channel to connect on that account
|
||||
#How to specify them for the different bridges:
|
||||
|
||||
# channel to connect on that account
|
||||
# How to specify them for the different bridges:
|
||||
#
|
||||
#irc - #channel (# is required) (this needs to be lowercase!)
|
||||
#mattermost - channel (the channel name as seen in the URL, not the displayname)
|
||||
#gitter - username/room
|
||||
#xmpp - channel
|
||||
#slack - channel (without the #)
|
||||
# - ID:C123456 (where C123456 is the channel ID) does not work with webhook
|
||||
#discord - channel (without the #)
|
||||
# - ID:123456789 (where 123456789 is the channel ID)
|
||||
# irc - #channel (# is required) (this needs to be lowercase!)
|
||||
# mattermost - channel (the channel name as seen in the URL, not the displayname)
|
||||
# gitter - username/room
|
||||
# xmpp - channel
|
||||
# slack - channel (without the #)
|
||||
# - ID:C123456 (where C123456 is the channel ID) does not work with webhook
|
||||
# discord - channel (without the #)
|
||||
# - ID:123456789 (where 123456789 is the channel ID)
|
||||
# (https://github.com/42wim/matterbridge/issues/57)
|
||||
#telegram - chatid (a large negative number, eg -123456789)
|
||||
# - category/channel (without the #) if you're using discord categories to group your channels
|
||||
# telegram - chatid (a large negative number, eg -123456789)
|
||||
# see (https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau)
|
||||
#hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
|
||||
#rocketchat - #channel (# is required (also needed for private channels!)
|
||||
#matrix - #channel:server (eg #yourchannel:matrix.org)
|
||||
# - encrypted rooms are not supported in matrix
|
||||
#steam - chatid (a large number).
|
||||
# hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
|
||||
# rocketchat - #channel (# is required (also needed for private channels!)
|
||||
# matrix - #channel:server (eg #yourchannel:matrix.org)
|
||||
# - encrypted rooms are not supported in matrix
|
||||
# steam - chatid (a large number).
|
||||
# The number in the URL when you click "enter chat room" in the browser
|
||||
#zulip - stream (without the #)
|
||||
# whatsapp - 48111222333-123455678999@g.us A unique group JID;
|
||||
# if you specify an empty string bridge will list all the possibilities
|
||||
# - "Group Name" if you specify a group name the bridge will hint its JID to specify
|
||||
# as group names might change in time and contain weird emoticons
|
||||
# zulip - stream/topic:topicname (without the #)
|
||||
#
|
||||
#REQUIRED
|
||||
# REQUIRED
|
||||
channel="#testing"
|
||||
|
||||
#OPTIONAL - only used for IRC and XMPP protocols at the moment
|
||||
@@ -1389,7 +1604,11 @@ enable=true
|
||||
|
||||
#OPTIONAL - webhookurl only works for discord (it needs a different URL for each cahnnel)
|
||||
[gateway.inout.options]
|
||||
webhookurl=""https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y"
|
||||
webhookurl="https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y"
|
||||
|
||||
[[gateway.inout]]
|
||||
account="zulip.streamchat"
|
||||
channel="general/topic:mytopic"
|
||||
|
||||
#API example
|
||||
#[[gateway.inout]]
|
||||
|
||||
227
matterclient/channels.go
Normal file
227
matterclient/channels.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package matterclient
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
// GetChannels returns all channels we're members off
|
||||
func (m *MMClient) GetChannels() []*model.Channel {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
var channels []*model.Channel
|
||||
// our primary team channels first
|
||||
channels = append(channels, m.Team.Channels...)
|
||||
for _, t := range m.OtherTeams {
|
||||
if t.Id != m.Team.Id {
|
||||
channels = append(channels, t.Channels...)
|
||||
}
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
func (m *MMClient) GetChannelHeader(channelId string) string { //nolint:golint
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
for _, t := range m.OtherTeams {
|
||||
for _, channel := range append(t.Channels, t.MoreChannels...) {
|
||||
if channel.Id == channelId {
|
||||
return channel.Header
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
if teamId != "" {
|
||||
return m.getChannelIdTeam(name, teamId)
|
||||
}
|
||||
|
||||
for _, t := range m.OtherTeams {
|
||||
for _, channel := range append(t.Channels, t.MoreChannels...) {
|
||||
if channel.Type == model.CHANNEL_GROUP {
|
||||
res := strings.Replace(channel.DisplayName, ", ", "-", -1)
|
||||
res = strings.Replace(res, " ", "_", -1)
|
||||
if res == name {
|
||||
return channel.Id
|
||||
}
|
||||
} else if channel.Name == name {
|
||||
return channel.Id
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MMClient) getChannelIdTeam(name string, teamId string) string { //nolint:golint
|
||||
for _, t := range m.OtherTeams {
|
||||
if t.Id == teamId {
|
||||
for _, channel := range append(t.Channels, t.MoreChannels...) {
|
||||
if channel.Name == name {
|
||||
return channel.Id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MMClient) GetChannelName(channelId string) string { //nolint:golint
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
for _, t := range m.OtherTeams {
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
for _, channel := range append(t.Channels, t.MoreChannels...) {
|
||||
if channel.Id == channelId {
|
||||
if channel.Type == model.CHANNEL_GROUP {
|
||||
res := strings.Replace(channel.DisplayName, ", ", "-", -1)
|
||||
res = strings.Replace(res, " ", "_", -1)
|
||||
return res
|
||||
}
|
||||
return channel.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MMClient) GetChannelTeamId(id string) string { //nolint:golint
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
for _, t := range append(m.OtherTeams, m.Team) {
|
||||
for _, channel := range append(t.Channels, t.MoreChannels...) {
|
||||
if channel.Id == id {
|
||||
return channel.TeamId
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MMClient) GetLastViewedAt(channelId string) int64 { //nolint:golint
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "")
|
||||
if resp.Error != nil {
|
||||
return model.GetMillis()
|
||||
}
|
||||
return res.LastViewedAt
|
||||
}
|
||||
|
||||
// GetMoreChannels returns existing channels where we're not a member off.
|
||||
func (m *MMClient) GetMoreChannels() []*model.Channel {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
var channels []*model.Channel
|
||||
for _, t := range m.OtherTeams {
|
||||
channels = append(channels, t.MoreChannels...)
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId).
|
||||
func (m *MMClient) GetTeamFromChannel(channelId string) string { //nolint:golint
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
var channels []*model.Channel
|
||||
for _, t := range m.OtherTeams {
|
||||
channels = append(channels, t.Channels...)
|
||||
if t.MoreChannels != nil {
|
||||
channels = append(channels, t.MoreChannels...)
|
||||
}
|
||||
for _, c := range channels {
|
||||
if c.Id == channelId {
|
||||
if c.Type == model.CHANNEL_GROUP {
|
||||
return "G"
|
||||
}
|
||||
return t.Id
|
||||
}
|
||||
}
|
||||
channels = nil
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
for _, c := range m.Team.Channels {
|
||||
if c.Id == channelId {
|
||||
m.logger.Debug("Not joining ", channelId, " already joined.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
m.logger.Debug("Joining ", channelId)
|
||||
_, resp := m.Client.AddChannelMember(channelId, m.User.Id)
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MMClient) 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 {
|
||||
if err := m.UpdateChannelsTeam(m.Team.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range m.OtherTeams {
|
||||
if err := m.UpdateChannelsTeam(t.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MMClient) UpdateChannelHeader(channelId string, header string) { //nolint:golint
|
||||
channel := &model.Channel{Id: channelId, Header: header}
|
||||
m.logger.Debugf("updating channelheader %#v, %#v", channelId, header)
|
||||
_, resp := m.Client.UpdateChannel(channel)
|
||||
if resp.Error != nil {
|
||||
m.logger.Error(resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MMClient) UpdateLastViewed(channelId string) error { //nolint:golint
|
||||
m.logger.Debugf("posting lastview %#v", channelId)
|
||||
view := &model.ChannelView{ChannelId: channelId}
|
||||
_, resp := m.Client.ViewChannel(m.User.Id, view)
|
||||
if resp.Error != nil {
|
||||
m.logger.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error)
|
||||
return resp.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
297
matterclient/helpers.go
Normal file
297
matterclient/helpers.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package matterclient
|
||||
|
||||
import (
|
||||
"crypto/md5" //nolint:gosec
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/jpillora/backoff"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error {
|
||||
var resp *model.Response
|
||||
var appErr *model.AppError
|
||||
var logmsg = "trying login"
|
||||
var err error
|
||||
for {
|
||||
m.logger.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
|
||||
if m.Credentials.Token != "" {
|
||||
resp, err = m.doLoginToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
|
||||
}
|
||||
appErr = resp.Error
|
||||
if appErr != nil {
|
||||
d := b.Duration()
|
||||
m.logger.Debug(appErr.DetailedError)
|
||||
if firstConnection {
|
||||
if appErr.Message == "" {
|
||||
return errors.New(appErr.DetailedError)
|
||||
}
|
||||
return errors.New(appErr.Message)
|
||||
}
|
||||
m.logger.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
|
||||
time.Sleep(d)
|
||||
logmsg = "retrying login"
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
// reset timer
|
||||
b.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MMClient) doLoginToken() (*model.Response, error) {
|
||||
var resp *model.Response
|
||||
var logmsg = "trying login"
|
||||
m.Client.AuthType = model.HEADER_BEARER
|
||||
m.Client.AuthToken = m.Credentials.Token
|
||||
if m.Credentials.CookieToken {
|
||||
m.logger.Debugf(logmsg + " with cookie (MMAUTH) token")
|
||||
m.Client.HttpClient.Jar = m.createCookieJar(m.Credentials.Token)
|
||||
} else {
|
||||
m.logger.Debugf(logmsg + " with personal token")
|
||||
}
|
||||
m.User, resp = m.Client.GetMe("")
|
||||
if resp.Error != nil {
|
||||
return resp, resp.Error
|
||||
}
|
||||
if m.User == nil {
|
||||
m.logger.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
|
||||
return resp, errors.New("invalid token")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *MMClient) handleLoginToken() error {
|
||||
switch {
|
||||
case strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN):
|
||||
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
|
||||
if len(token) != 2 {
|
||||
return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
|
||||
}
|
||||
m.Credentials.Token = token[1]
|
||||
m.Credentials.CookieToken = true
|
||||
case strings.Contains(m.Credentials.Pass, "token="):
|
||||
token := strings.Split(m.Credentials.Pass, "token=")
|
||||
if len(token) != 2 {
|
||||
return errors.New("incorrect personal token. valid input is token=yourtoken")
|
||||
}
|
||||
m.Credentials.Token = token[1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MMClient) initClient(firstConnection bool, b *backoff.Backoff) error {
|
||||
uriScheme := "https://"
|
||||
if m.NoTLS {
|
||||
uriScheme = "http://"
|
||||
}
|
||||
// login to mattermost
|
||||
m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server)
|
||||
m.Client.HttpClient.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
m.Client.HttpClient.Timeout = time.Second * 10
|
||||
|
||||
// handle MMAUTHTOKEN and personal token
|
||||
if err := m.handleLoginToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if server alive, retry until
|
||||
if err := m.serverAlive(firstConnection, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initialize user and teams
|
||||
func (m *MMClient) initUser() error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
// we only load all team data on initial login.
|
||||
// all other updates are for channels from our (primary) team only.
|
||||
//m.logger.Debug("initUser(): loading all team data")
|
||||
teams, resp := m.Client.GetTeamsForUser(m.User.Id, "")
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
for _, team := range teams {
|
||||
idx := 0
|
||||
max := 200
|
||||
usermap := make(map[string]*model.User)
|
||||
mmusers, resp := m.Client.GetUsersInTeam(team.Id, idx, max, "")
|
||||
if resp.Error != nil {
|
||||
return errors.New(resp.Error.DetailedError)
|
||||
}
|
||||
for len(mmusers) > 0 {
|
||||
for _, user := range mmusers {
|
||||
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}
|
||||
|
||||
mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, "")
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
t.Channels = mmchannels
|
||||
mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "")
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
t.MoreChannels = mmchannels
|
||||
m.OtherTeams = append(m.OtherTeams, t)
|
||||
if team.Name == m.Credentials.Team {
|
||||
m.Team = t
|
||||
m.logger.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id)
|
||||
}
|
||||
// add all users
|
||||
for k, v := range t.Users {
|
||||
m.Users[k] = v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error {
|
||||
defer b.Reset()
|
||||
for {
|
||||
d := b.Duration()
|
||||
// bogus call to get the serverversion
|
||||
_, resp := m.Client.Logout()
|
||||
if resp.Error != nil {
|
||||
return fmt.Errorf("%#v", resp.Error.Error())
|
||||
}
|
||||
if firstConnection && !m.SkipVersionCheck && !supportedVersion(resp.ServerVersion) {
|
||||
return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion)
|
||||
}
|
||||
if !m.SkipVersionCheck {
|
||||
m.ServerVersion = resp.ServerVersion
|
||||
if m.ServerVersion == "" {
|
||||
m.logger.Debugf("Server not up yet, reconnecting in %s", d)
|
||||
time.Sleep(d)
|
||||
} else {
|
||||
m.logger.Infof("Found version %s", m.ServerVersion)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MMClient) wsConnect() {
|
||||
b := &backoff.Backoff{
|
||||
Min: time.Second,
|
||||
Max: 5 * time.Minute,
|
||||
Jitter: true,
|
||||
}
|
||||
|
||||
m.WsConnected = false
|
||||
wsScheme := "wss://"
|
||||
if m.NoTLS {
|
||||
wsScheme = "ws://"
|
||||
}
|
||||
|
||||
// setup websocket connection
|
||||
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket"
|
||||
header := http.Header{}
|
||||
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
|
||||
|
||||
m.logger.Debugf("WsClient: making connection: %s", wsurl)
|
||||
for {
|
||||
wsDialer := &websocket.Dialer{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
var err error
|
||||
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
|
||||
if err != nil {
|
||||
d := b.Duration()
|
||||
m.logger.Debugf("WSS: %s, reconnecting in %s", err, d)
|
||||
time.Sleep(d)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
m.logger.Debug("WsClient: connected")
|
||||
m.WsSequence = 1
|
||||
m.WsPingChan = make(chan *model.WebSocketResponse)
|
||||
// only start to parse WS messages when login is completely done
|
||||
m.WsConnected = true
|
||||
}
|
||||
|
||||
func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
|
||||
var cookies []*http.Cookie
|
||||
jar, _ := cookiejar.New(nil)
|
||||
firstCookie := &http.Cookie{
|
||||
Name: "MMAUTHTOKEN",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
Domain: m.Credentials.Server,
|
||||
}
|
||||
cookies = append(cookies, firstCookie)
|
||||
cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
|
||||
jar.SetCookies(cookieURL, cookies)
|
||||
return jar
|
||||
}
|
||||
|
||||
func (m *MMClient) checkAlive() error {
|
||||
// check if session still is valid
|
||||
_, resp := m.Client.GetMe("")
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
m.logger.Debug("WS PING")
|
||||
return m.sendWSRequest("ping", nil)
|
||||
}
|
||||
|
||||
func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error {
|
||||
req := &model.WebSocketRequest{}
|
||||
req.Seq = m.WsSequence
|
||||
req.Action = action
|
||||
req.Data = data
|
||||
m.WsSequence++
|
||||
m.logger.Debugf("sendWsRequest %#v", req)
|
||||
return m.WsClient.WriteJSON(req)
|
||||
}
|
||||
|
||||
func supportedVersion(version string) bool {
|
||||
if strings.HasPrefix(version, "3.8.0") ||
|
||||
strings.HasPrefix(version, "3.9.0") ||
|
||||
strings.HasPrefix(version, "3.10.0") ||
|
||||
strings.HasPrefix(version, "4.") ||
|
||||
strings.HasPrefix(version, "5.") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func digestString(s string) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(s))) //nolint:gosec
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
207
matterclient/messages.go
Normal file
207
matterclient/messages.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package matterclient
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
func (m *MMClient) parseActionPost(rmsg *Message) {
|
||||
// add post to cache, if it already exists don't relay this again.
|
||||
// this should fix reposts
|
||||
if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok {
|
||||
m.logger.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string))
|
||||
rmsg.Text = ""
|
||||
return
|
||||
}
|
||||
data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string)))
|
||||
// we don't have the user, refresh the userlist
|
||||
if m.GetUser(data.UserId) == nil {
|
||||
m.logger.Infof("User '%v' is not known, ignoring message '%#v'",
|
||||
data.UserId, data)
|
||||
return
|
||||
}
|
||||
rmsg.Username = m.GetUserName(data.UserId)
|
||||
rmsg.Channel = m.GetChannelName(data.ChannelId)
|
||||
rmsg.UserID = data.UserId
|
||||
rmsg.Type = data.Type
|
||||
teamid, _ := rmsg.Raw.Data["team_id"].(string)
|
||||
// edit messsages have no team_id for some reason
|
||||
if teamid == "" {
|
||||
// we can find the team_id from the channelid
|
||||
teamid = m.GetChannelTeamId(data.ChannelId)
|
||||
rmsg.Raw.Data["team_id"] = teamid
|
||||
}
|
||||
if teamid != "" {
|
||||
rmsg.Team = m.GetTeamName(teamid)
|
||||
}
|
||||
// direct message
|
||||
if rmsg.Raw.Data["channel_type"] == "D" {
|
||||
rmsg.Channel = m.GetUser(data.UserId).Username
|
||||
}
|
||||
rmsg.Text = data.Message
|
||||
rmsg.Post = data
|
||||
}
|
||||
|
||||
func (m *MMClient) parseMessage(rmsg *Message) {
|
||||
switch rmsg.Raw.Event {
|
||||
case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED:
|
||||
m.parseActionPost(rmsg)
|
||||
case "user_updated":
|
||||
user := rmsg.Raw.Data["user"].(map[string]interface{})
|
||||
if _, ok := user["id"].(string); ok {
|
||||
m.UpdateUser(user["id"].(string))
|
||||
}
|
||||
case "group_added":
|
||||
if err := m.UpdateChannels(); err != nil {
|
||||
m.logger.Errorf("failed to update channels: %#v", err)
|
||||
}
|
||||
/*
|
||||
case model.ACTION_USER_REMOVED:
|
||||
m.handleWsActionUserRemoved(&rmsg)
|
||||
case model.ACTION_USER_ADDED:
|
||||
m.handleWsActionUserAdded(&rmsg)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) {
|
||||
if rmsg.Data != nil {
|
||||
// ping reply
|
||||
if rmsg.Data["text"].(string) == "pong" {
|
||||
m.WsPingChan <- &rmsg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MMClient) DeleteMessage(postId string) error { //nolint:golint
|
||||
_, resp := m.Client.DeletePost(postId)
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint
|
||||
post := &model.Post{Message: text, Id: postId}
|
||||
res, resp := m.Client.UpdatePost(postId, post)
|
||||
if resp.Error != nil {
|
||||
return "", resp.Error
|
||||
}
|
||||
return res.Id, nil
|
||||
}
|
||||
|
||||
func (m *MMClient) GetFileLinks(filenames []string) []string {
|
||||
uriScheme := "https://"
|
||||
if m.NoTLS {
|
||||
uriScheme = "http://"
|
||||
}
|
||||
|
||||
var output []string
|
||||
for _, f := range filenames {
|
||||
res, resp := m.Client.GetFileLink(f)
|
||||
if resp.Error != nil {
|
||||
// public links is probably disabled, create the link ourselves
|
||||
output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V4+"/files/"+f)
|
||||
continue
|
||||
}
|
||||
output = append(output, res)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList { //nolint:golint
|
||||
res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "")
|
||||
if resp.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList { //nolint:golint
|
||||
res, resp := m.Client.GetPostsSince(channelId, time)
|
||||
if resp.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (m *MMClient) GetPublicLink(filename string) string {
|
||||
res, resp := m.Client.GetFileLink(filename)
|
||||
if resp.Error != nil {
|
||||
return ""
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (m *MMClient) GetPublicLinks(filenames []string) []string {
|
||||
var output []string
|
||||
for _, f := range filenames {
|
||||
res, resp := m.Client.GetFileLink(f)
|
||||
if resp.Error != nil {
|
||||
continue
|
||||
}
|
||||
output = append(output, res)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (m *MMClient) PostMessage(channelId string, text string, rootId string) (string, error) { //nolint:golint
|
||||
post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId}
|
||||
res, resp := m.Client.CreatePost(post)
|
||||
if resp.Error != nil {
|
||||
return "", resp.Error
|
||||
}
|
||||
return res.Id, nil
|
||||
}
|
||||
|
||||
func (m *MMClient) PostMessageWithFiles(channelId string, text string, rootId string, fileIds []string) (string, error) { //nolint:golint
|
||||
post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId, FileIds: fileIds}
|
||||
res, resp := m.Client.CreatePost(post)
|
||||
if resp.Error != nil {
|
||||
return "", resp.Error
|
||||
}
|
||||
return res.Id, nil
|
||||
}
|
||||
|
||||
func (m *MMClient) SearchPosts(query string) *model.PostList {
|
||||
res, resp := m.Client.SearchPosts(m.Team.Id, query, false)
|
||||
if resp.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// SendDirectMessage sends a direct message to specified user
|
||||
func (m *MMClient) SendDirectMessage(toUserId string, msg string, rootId string) { //nolint:golint
|
||||
m.SendDirectMessageProps(toUserId, msg, rootId, nil)
|
||||
}
|
||||
|
||||
func (m *MMClient) SendDirectMessageProps(toUserId string, msg string, rootId string, props map[string]interface{}) { //nolint:golint
|
||||
m.logger.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
|
||||
// create DM channel (only happens on first message)
|
||||
_, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId)
|
||||
if resp.Error != nil {
|
||||
m.logger.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error)
|
||||
return
|
||||
}
|
||||
channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
|
||||
|
||||
// update our channels
|
||||
if err := m.UpdateChannels(); err != nil {
|
||||
m.logger.Errorf("failed to update channels: %#v", err)
|
||||
}
|
||||
|
||||
// build & send the message
|
||||
msg = strings.Replace(msg, "\r", "", -1)
|
||||
post := &model.Post{ChannelId: m.GetChannelId(channelName, m.Team.Id), Message: msg, RootId: rootId, Props: props}
|
||||
m.Client.CreatePost(post)
|
||||
}
|
||||
|
||||
func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) { //nolint:golint
|
||||
f, resp := m.Client.UploadFile(data, channelId, filename)
|
||||
if resp.Error != nil {
|
||||
return "", resp.Error
|
||||
}
|
||||
return f.FileInfos[0].Id, nil
|
||||
}
|
||||
165
matterclient/users.go
Normal file
165
matterclient/users.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package matterclient
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
func (m *MMClient) GetNickName(userId string) string { //nolint:golint
|
||||
user := m.GetUser(userId)
|
||||
if user != nil {
|
||||
return user.Nickname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MMClient) GetStatus(userId string) string { //nolint:golint
|
||||
res, resp := m.Client.GetUserStatus(userId, "")
|
||||
if resp.Error != nil {
|
||||
return ""
|
||||
}
|
||||
if res.Status == model.STATUS_AWAY {
|
||||
return "away"
|
||||
}
|
||||
if res.Status == model.STATUS_ONLINE {
|
||||
return "online"
|
||||
}
|
||||
return "offline"
|
||||
}
|
||||
|
||||
func (m *MMClient) GetStatuses() map[string]string {
|
||||
var ids []string
|
||||
statuses := make(map[string]string)
|
||||
for id := range m.Users {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
res, resp := m.Client.GetUsersStatusesByIds(ids)
|
||||
if resp.Error != nil {
|
||||
return statuses
|
||||
}
|
||||
for _, status := range res {
|
||||
statuses[status.UserId] = "offline"
|
||||
if status.Status == model.STATUS_AWAY {
|
||||
statuses[status.UserId] = "away"
|
||||
}
|
||||
if status.Status == model.STATUS_ONLINE {
|
||||
statuses[status.UserId] = "online"
|
||||
}
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
|
||||
func (m *MMClient) GetTeamId() string { //nolint:golint
|
||||
return m.Team.Id
|
||||
}
|
||||
|
||||
// GetTeamName returns the name of the specified teamId
|
||||
func (m *MMClient) GetTeamName(teamId string) string { //nolint:golint
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
for _, t := range m.OtherTeams {
|
||||
if t.Id == teamId {
|
||||
return t.Team.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MMClient) GetUser(userId string) *model.User { //nolint:golint
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
_, ok := m.Users[userId]
|
||||
if !ok {
|
||||
res, resp := m.Client.GetUser(userId, "")
|
||||
if resp.Error != nil {
|
||||
return nil
|
||||
}
|
||||
m.Users[userId] = res
|
||||
}
|
||||
return m.Users[userId]
|
||||
}
|
||||
|
||||
func (m *MMClient) GetUserName(userId string) string { //nolint:golint
|
||||
user := m.GetUser(userId)
|
||||
if user != nil {
|
||||
return user.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MMClient) GetUsers() map[string]*model.User {
|
||||
users := make(map[string]*model.User)
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
for k, v := range m.Users {
|
||||
users[k] = v
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func (m *MMClient) UpdateUsers() error {
|
||||
idx := 0
|
||||
max := 200
|
||||
mmusers, resp := m.Client.GetUsers(idx, max, "")
|
||||
if resp.Error != nil {
|
||||
return errors.New(resp.Error.DetailedError)
|
||||
}
|
||||
for len(mmusers) > 0 {
|
||||
m.Lock()
|
||||
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++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MMClient) UpdateUserNick(nick string) error {
|
||||
user := m.User
|
||||
user.Nickname = nick
|
||||
_, resp := m.Client.UpdateUser(user)
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MMClient) UsernamesInChannel(channelId string) []string { //nolint:golint
|
||||
res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "")
|
||||
if resp.Error != nil {
|
||||
m.logger.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error)
|
||||
return []string{}
|
||||
}
|
||||
allusers := m.GetUsers()
|
||||
result := []string{}
|
||||
for _, member := range *res {
|
||||
result = append(result, allusers[member.UserId].Nickname)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *MMClient) UpdateStatus(userId string, status string) error { //nolint:golint
|
||||
_, resp := m.Client.UpdateUserStatus(userId, &model.Status{Status: status})
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MMClient) UpdateUser(userId string) { //nolint:golint
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
res, resp := m.Client.GetUser(userId, "")
|
||||
if resp.Error != nil {
|
||||
return
|
||||
}
|
||||
m.Users[userId] = res
|
||||
}
|
||||
@@ -71,7 +71,7 @@ type Config struct {
|
||||
func New(url string, config Config) *Client {
|
||||
c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}, //nolint:gosec
|
||||
}
|
||||
c.httpclient = &http.Client{Transport: tr}
|
||||
if !c.DisableServer {
|
||||
|
||||
24
vendor/github.com/Baozisoftware/qrcode-terminal-go/.gitignore
generated
vendored
Normal file
24
vendor/github.com/Baozisoftware/qrcode-terminal-go/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
29
vendor/github.com/Baozisoftware/qrcode-terminal-go/LICENSE
generated
vendored
Normal file
29
vendor/github.com/Baozisoftware/qrcode-terminal-go/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2017, Baozisoftware
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
39
vendor/github.com/Baozisoftware/qrcode-terminal-go/README.md
generated
vendored
Normal file
39
vendor/github.com/Baozisoftware/qrcode-terminal-go/README.md
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
# qrcode-terminal-go
|
||||
QRCode terminal for golang.
|
||||
|
||||
# Example
|
||||
```go
|
||||
package main
|
||||
|
||||
import "github.com/Baozisoftware/qrcode-terminal-go"
|
||||
|
||||
func main() {
|
||||
Test1()
|
||||
Test2()
|
||||
}
|
||||
|
||||
func Test1(){
|
||||
content := "Hello, 世界"
|
||||
obj := qrcodeTerminal.New()
|
||||
obj.Get(content).Print()
|
||||
}
|
||||
|
||||
func Test2(){
|
||||
content := "https://github.com/Baozisoftware/qrcode-terminal-go"
|
||||
obj := qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightBlue,qrcodeTerminal.ConsoleColors.BrightGreen,qrcodeTerminal.QRCodeRecoveryLevels.Low)
|
||||
obj.Get([]byte(content)).Print()
|
||||
}
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
### Windows XP
|
||||

|
||||
### Windows 7
|
||||

|
||||
### Windows 10
|
||||

|
||||
### Ubuntu
|
||||

|
||||
### macOS
|
||||

|
||||
155
vendor/github.com/Baozisoftware/qrcode-terminal-go/qrcodeTerminal.go
generated
vendored
Normal file
155
vendor/github.com/Baozisoftware/qrcode-terminal-go/qrcodeTerminal.go
generated
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
package qrcodeTerminal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
"github.com/mattn/go-colorable"
|
||||
"image/png"
|
||||
nbytes "bytes"
|
||||
)
|
||||
|
||||
type consoleColor string
|
||||
type consoleColors struct {
|
||||
NormalBlack consoleColor
|
||||
NormalRed consoleColor
|
||||
NormalGreen consoleColor
|
||||
NormalYellow consoleColor
|
||||
NormalBlue consoleColor
|
||||
NormalMagenta consoleColor
|
||||
NormalCyan consoleColor
|
||||
NormalWhite consoleColor
|
||||
BrightBlack consoleColor
|
||||
BrightRed consoleColor
|
||||
BrightGreen consoleColor
|
||||
BrightYellow consoleColor
|
||||
BrightBlue consoleColor
|
||||
BrightMagenta consoleColor
|
||||
BrightCyan consoleColor
|
||||
BrightWhite consoleColor
|
||||
}
|
||||
type qrcodeRecoveryLevel qrcode.RecoveryLevel
|
||||
type qrcodeRecoveryLevels struct {
|
||||
Low qrcodeRecoveryLevel
|
||||
Medium qrcodeRecoveryLevel
|
||||
High qrcodeRecoveryLevel
|
||||
Highest qrcodeRecoveryLevel
|
||||
}
|
||||
|
||||
var (
|
||||
ConsoleColors consoleColors = consoleColors{
|
||||
NormalBlack: "\033[38;5;0m \033[0m",
|
||||
NormalRed: "\033[38;5;1m \033[0m",
|
||||
NormalGreen: "\033[38;5;2m \033[0m",
|
||||
NormalYellow: "\033[38;5;3m \033[0m",
|
||||
NormalBlue: "\033[38;5;4m \033[0m",
|
||||
NormalMagenta: "\033[38;5;5m \033[0m",
|
||||
NormalCyan: "\033[38;5;6m \033[0m",
|
||||
NormalWhite: "\033[38;5;7m \033[0m",
|
||||
BrightBlack: "\033[48;5;0m \033[0m",
|
||||
BrightRed: "\033[48;5;1m \033[0m",
|
||||
BrightGreen: "\033[48;5;2m \033[0m",
|
||||
BrightYellow: "\033[48;5;3m \033[0m",
|
||||
BrightBlue: "\033[48;5;4m \033[0m",
|
||||
BrightMagenta: "\033[48;5;5m \033[0m",
|
||||
BrightCyan: "\033[48;5;6m \033[0m",
|
||||
BrightWhite: "\033[48;5;7m \033[0m"}
|
||||
QRCodeRecoveryLevels = qrcodeRecoveryLevels{
|
||||
Low: qrcodeRecoveryLevel(qrcode.Low),
|
||||
Medium: qrcodeRecoveryLevel(qrcode.Medium),
|
||||
High: qrcodeRecoveryLevel(qrcode.High),
|
||||
Highest: qrcodeRecoveryLevel(qrcode.Highest)}
|
||||
)
|
||||
|
||||
type QRCodeString string
|
||||
|
||||
func (v *QRCodeString) Print() {
|
||||
fmt.Fprint(outer, *v)
|
||||
}
|
||||
|
||||
type qrcodeTerminal struct {
|
||||
front consoleColor
|
||||
back consoleColor
|
||||
level qrcodeRecoveryLevel
|
||||
}
|
||||
|
||||
func (v *qrcodeTerminal) Get(content interface{}) (result *QRCodeString) {
|
||||
var qr *qrcode.QRCode
|
||||
var err error
|
||||
if t, ok := content.(string); ok {
|
||||
qr, err = qrcode.New(t, qrcode.RecoveryLevel(v.level))
|
||||
} else if t, ok := content.([]byte); ok {
|
||||
qr, err = qrcode.New(string(t), qrcode.RecoveryLevel(v.level))
|
||||
}
|
||||
if qr != nil && err == nil {
|
||||
data := qr.Bitmap()
|
||||
result = v.getQRCodeString(data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (v *qrcodeTerminal) Get2(bytes []byte) (result *QRCodeString) {
|
||||
data, err := parseQR(bytes)
|
||||
if err == nil {
|
||||
result = v.getQRCodeString(data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func New2(front, back consoleColor, level qrcodeRecoveryLevel) *qrcodeTerminal {
|
||||
obj := qrcodeTerminal{front: front, back: back, level: level}
|
||||
return &obj
|
||||
}
|
||||
|
||||
func New() *qrcodeTerminal {
|
||||
front, back, level := ConsoleColors.BrightBlack, ConsoleColors.BrightWhite, QRCodeRecoveryLevels.Medium
|
||||
return New2(front, back, level)
|
||||
}
|
||||
|
||||
func (v *qrcodeTerminal) getQRCodeString(data [][]bool) (result *QRCodeString) {
|
||||
str := ""
|
||||
for ir, row := range data {
|
||||
lr := len(row)
|
||||
if ir == 0 || ir == 1 || ir == 2 ||
|
||||
ir == lr-1 || ir == lr-2 || ir == lr-3 {
|
||||
continue
|
||||
}
|
||||
for ic, col := range row {
|
||||
lc := len(data)
|
||||
if ic == 0 || ic == 1 || ic == 2 ||
|
||||
ic == lc-1 || ic == lc-2 || ic == lc-3 {
|
||||
continue
|
||||
}
|
||||
if col {
|
||||
str += fmt.Sprint(v.front)
|
||||
} else {
|
||||
str += fmt.Sprint(v.back)
|
||||
}
|
||||
}
|
||||
str += fmt.Sprintln()
|
||||
}
|
||||
obj := QRCodeString(str)
|
||||
result = &obj
|
||||
return
|
||||
}
|
||||
|
||||
func parseQR(bytes []byte) (data [][]bool, err error) {
|
||||
r := nbytes.NewReader(bytes)
|
||||
img, err := png.Decode(r)
|
||||
if err == nil {
|
||||
rect := img.Bounds()
|
||||
mx, my := rect.Max.X, rect.Max.Y
|
||||
data = make([][]bool, mx)
|
||||
for x := 0; x < mx; x++ {
|
||||
data[x] = make([]bool, my)
|
||||
for y := 0; y < my; y++ {
|
||||
c := img.At(x, y)
|
||||
r, _, _, _ := c.RGBA()
|
||||
data[x][y] = r == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var outer = colorable.NewColorableStdout()
|
||||
19
vendor/github.com/Jeffail/gabs/LICENSE
generated
vendored
Normal file
19
vendor/github.com/Jeffail/gabs/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2014 Ashley Jeffs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
315
vendor/github.com/Jeffail/gabs/README.md
generated
vendored
Normal file
315
vendor/github.com/Jeffail/gabs/README.md
generated
vendored
Normal file
@@ -0,0 +1,315 @@
|
||||

|
||||
|
||||
Gabs is a small utility for dealing with dynamic or unknown JSON structures in
|
||||
golang. It's pretty much just a helpful wrapper around the golang
|
||||
`json.Marshal/json.Unmarshal` behaviour and `map[string]interface{}` objects.
|
||||
It does nothing spectacular except for being fabulous.
|
||||
|
||||
https://godoc.org/github.com/Jeffail/gabs
|
||||
|
||||
## How to install:
|
||||
|
||||
``` bash
|
||||
go get github.com/Jeffail/gabs
|
||||
```
|
||||
|
||||
## How to use
|
||||
|
||||
### Parsing and searching JSON
|
||||
|
||||
``` go
|
||||
...
|
||||
|
||||
import "github.com/Jeffail/gabs"
|
||||
|
||||
jsonParsed, err := gabs.ParseJSON([]byte(`{
|
||||
"outter":{
|
||||
"inner":{
|
||||
"value1":10,
|
||||
"value2":22
|
||||
},
|
||||
"alsoInner":{
|
||||
"value1":20
|
||||
}
|
||||
}
|
||||
}`))
|
||||
|
||||
var value float64
|
||||
var ok bool
|
||||
|
||||
value, ok = jsonParsed.Path("outter.inner.value1").Data().(float64)
|
||||
// value == 10.0, ok == true
|
||||
|
||||
value, ok = jsonParsed.Search("outter", "inner", "value1").Data().(float64)
|
||||
// value == 10.0, ok == true
|
||||
|
||||
value, ok = jsonParsed.Path("does.not.exist").Data().(float64)
|
||||
// value == 0.0, ok == false
|
||||
|
||||
exists := jsonParsed.Exists("outter", "inner", "value1")
|
||||
// exists == true
|
||||
|
||||
exists := jsonParsed.Exists("does", "not", "exist")
|
||||
// exists == false
|
||||
|
||||
exists := jsonParsed.ExistsP("does.not.exist")
|
||||
// exists == false
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Iterating objects
|
||||
|
||||
``` go
|
||||
...
|
||||
|
||||
jsonParsed, _ := gabs.ParseJSON([]byte(`{"object":{ "first": 1, "second": 2, "third": 3 }}`))
|
||||
|
||||
// S is shorthand for Search
|
||||
children, _ := jsonParsed.S("object").ChildrenMap()
|
||||
for key, child := range children {
|
||||
fmt.Printf("key: %v, value: %v\n", key, child.Data().(string))
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Iterating arrays
|
||||
|
||||
``` go
|
||||
...
|
||||
|
||||
jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ "first", "second", "third" ]}`))
|
||||
|
||||
// S is shorthand for Search
|
||||
children, _ := jsonParsed.S("array").Children()
|
||||
for _, child := range children {
|
||||
fmt.Println(child.Data().(string))
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Will print:
|
||||
|
||||
```
|
||||
first
|
||||
second
|
||||
third
|
||||
```
|
||||
|
||||
Children() will return all children of an array in order. This also works on
|
||||
objects, however, the children will be returned in a random order.
|
||||
|
||||
### Searching through arrays
|
||||
|
||||
If your JSON structure contains arrays you can still search the fields of the
|
||||
objects within the array, this returns a JSON array containing the results for
|
||||
each element.
|
||||
|
||||
``` go
|
||||
...
|
||||
|
||||
jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ {"value":1}, {"value":2}, {"value":3} ]}`))
|
||||
fmt.Println(jsonParsed.Path("array.value").String())
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Will print:
|
||||
|
||||
```
|
||||
[1,2,3]
|
||||
```
|
||||
|
||||
### Generating JSON
|
||||
|
||||
``` go
|
||||
...
|
||||
|
||||
jsonObj := gabs.New()
|
||||
// or gabs.Consume(jsonObject) to work on an existing map[string]interface{}
|
||||
|
||||
jsonObj.Set(10, "outter", "inner", "value")
|
||||
jsonObj.SetP(20, "outter.inner.value2")
|
||||
jsonObj.Set(30, "outter", "inner2", "value3")
|
||||
|
||||
fmt.Println(jsonObj.String())
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Will print:
|
||||
|
||||
```
|
||||
{"outter":{"inner":{"value":10,"value2":20},"inner2":{"value3":30}}}
|
||||
```
|
||||
|
||||
To pretty-print:
|
||||
|
||||
``` go
|
||||
...
|
||||
|
||||
fmt.Println(jsonObj.StringIndent("", " "))
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Will print:
|
||||
|
||||
```
|
||||
{
|
||||
"outter": {
|
||||
"inner": {
|
||||
"value": 10,
|
||||
"value2": 20
|
||||
},
|
||||
"inner2": {
|
||||
"value3": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generating Arrays
|
||||
|
||||
``` go
|
||||
...
|
||||
|
||||
jsonObj := gabs.New()
|
||||
|
||||
jsonObj.Array("foo", "array")
|
||||
// Or .ArrayP("foo.array")
|
||||
|
||||
jsonObj.ArrayAppend(10, "foo", "array")
|
||||
jsonObj.ArrayAppend(20, "foo", "array")
|
||||
jsonObj.ArrayAppend(30, "foo", "array")
|
||||
|
||||
fmt.Println(jsonObj.String())
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Will print:
|
||||
|
||||
```
|
||||
{"foo":{"array":[10,20,30]}}
|
||||
```
|
||||
|
||||
Working with arrays by index:
|
||||
|
||||
``` go
|
||||
...
|
||||
|
||||
jsonObj := gabs.New()
|
||||
|
||||
// Create an array with the length of 3
|
||||
jsonObj.ArrayOfSize(3, "foo")
|
||||
|
||||
jsonObj.S("foo").SetIndex("test1", 0)
|
||||
jsonObj.S("foo").SetIndex("test2", 1)
|
||||
|
||||
// Create an embedded array with the length of 3
|
||||
jsonObj.S("foo").ArrayOfSizeI(3, 2)
|
||||
|
||||
jsonObj.S("foo").Index(2).SetIndex(1, 0)
|
||||
jsonObj.S("foo").Index(2).SetIndex(2, 1)
|
||||
jsonObj.S("foo").Index(2).SetIndex(3, 2)
|
||||
|
||||
fmt.Println(jsonObj.String())
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Will print:
|
||||
|
||||
```
|
||||
{"foo":["test1","test2",[1,2,3]]}
|
||||
```
|
||||
|
||||
### Converting back to JSON
|
||||
|
||||
This is the easiest part:
|
||||
|
||||
``` go
|
||||
...
|
||||
|
||||
jsonParsedObj, _ := gabs.ParseJSON([]byte(`{
|
||||
"outter":{
|
||||
"values":{
|
||||
"first":10,
|
||||
"second":11
|
||||
}
|
||||
},
|
||||
"outter2":"hello world"
|
||||
}`))
|
||||
|
||||
jsonOutput := jsonParsedObj.String()
|
||||
// Becomes `{"outter":{"values":{"first":10,"second":11}},"outter2":"hello world"}`
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
And to serialize a specific segment is as simple as:
|
||||
|
||||
``` go
|
||||
...
|
||||
|
||||
jsonParsedObj := gabs.ParseJSON([]byte(`{
|
||||
"outter":{
|
||||
"values":{
|
||||
"first":10,
|
||||
"second":11
|
||||
}
|
||||
},
|
||||
"outter2":"hello world"
|
||||
}`))
|
||||
|
||||
jsonOutput := jsonParsedObj.Search("outter").String()
|
||||
// Becomes `{"values":{"first":10,"second":11}}`
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Merge two containers
|
||||
|
||||
You can merge a JSON structure into an existing one, where collisions will be
|
||||
converted into a JSON array.
|
||||
|
||||
``` go
|
||||
jsonParsed1, _ := ParseJSON([]byte(`{"outter": {"value1": "one"}}`))
|
||||
jsonParsed2, _ := ParseJSON([]byte(`{"outter": {"inner": {"value3": "three"}}, "outter2": {"value2": "two"}}`))
|
||||
|
||||
jsonParsed1.Merge(jsonParsed2)
|
||||
// Becomes `{"outter":{"inner":{"value3":"three"},"value1":"one"},"outter2":{"value2":"two"}}`
|
||||
```
|
||||
|
||||
Arrays are merged:
|
||||
|
||||
``` go
|
||||
jsonParsed1, _ := ParseJSON([]byte(`{"array": ["one"]}`))
|
||||
jsonParsed2, _ := ParseJSON([]byte(`{"array": ["two"]}`))
|
||||
|
||||
jsonParsed1.Merge(jsonParsed2)
|
||||
// Becomes `{"array":["one", "two"]}`
|
||||
```
|
||||
|
||||
### Parsing Numbers
|
||||
|
||||
Gabs uses the `json` package under the bonnet, which by default will parse all
|
||||
number values into `float64`. If you need to parse `Int` values then you should
|
||||
use a `json.Decoder` (https://golang.org/pkg/encoding/json/#Decoder):
|
||||
|
||||
``` go
|
||||
sample := []byte(`{"test":{"int":10, "float":6.66}}`)
|
||||
dec := json.NewDecoder(bytes.NewReader(sample))
|
||||
dec.UseNumber()
|
||||
|
||||
val, err := gabs.ParseJSONDecoder(dec)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to parse: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
intValue, err := val.Path("test.int").Data().(json.Number).Int64()
|
||||
```
|
||||
581
vendor/github.com/Jeffail/gabs/gabs.go
generated
vendored
Normal file
581
vendor/github.com/Jeffail/gabs/gabs.go
generated
vendored
Normal file
@@ -0,0 +1,581 @@
|
||||
/*
|
||||
Copyright (c) 2014 Ashley Jeffs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Package gabs implements a simplified wrapper around creating and parsing JSON.
|
||||
package gabs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
// ErrOutOfBounds - Index out of bounds.
|
||||
ErrOutOfBounds = errors.New("out of bounds")
|
||||
|
||||
// ErrNotObjOrArray - The target is not an object or array type.
|
||||
ErrNotObjOrArray = errors.New("not an object or array")
|
||||
|
||||
// ErrNotObj - The target is not an object type.
|
||||
ErrNotObj = errors.New("not an object")
|
||||
|
||||
// ErrNotArray - The target is not an array type.
|
||||
ErrNotArray = errors.New("not an array")
|
||||
|
||||
// ErrPathCollision - Creating a path failed because an element collided with an existing value.
|
||||
ErrPathCollision = errors.New("encountered value collision whilst building path")
|
||||
|
||||
// ErrInvalidInputObj - The input value was not a map[string]interface{}.
|
||||
ErrInvalidInputObj = errors.New("invalid input object")
|
||||
|
||||
// ErrInvalidInputText - The input data could not be parsed.
|
||||
ErrInvalidInputText = errors.New("input text could not be parsed")
|
||||
|
||||
// ErrInvalidPath - The filepath was not valid.
|
||||
ErrInvalidPath = errors.New("invalid file path")
|
||||
|
||||
// ErrInvalidBuffer - The input buffer contained an invalid JSON string
|
||||
ErrInvalidBuffer = errors.New("input buffer contained invalid JSON")
|
||||
)
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
// Container - an internal structure that holds a reference to the core interface map of the parsed
|
||||
// json. Use this container to move context.
|
||||
type Container struct {
|
||||
object interface{}
|
||||
}
|
||||
|
||||
// Data - Return the contained data as an interface{}.
|
||||
func (g *Container) Data() interface{} {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
return g.object
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
// Path - Search for a value using dot notation.
|
||||
func (g *Container) Path(path string) *Container {
|
||||
return g.Search(strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
// Search - Attempt to find and return an object within the JSON structure by specifying the
|
||||
// hierarchy of field names to locate the target. If the search encounters an array and has not
|
||||
// reached the end target then it will iterate each object of the array for the target and return
|
||||
// all of the results in a JSON array.
|
||||
func (g *Container) Search(hierarchy ...string) *Container {
|
||||
var object interface{}
|
||||
|
||||
object = g.Data()
|
||||
for target := 0; target < len(hierarchy); target++ {
|
||||
if mmap, ok := object.(map[string]interface{}); ok {
|
||||
object, ok = mmap[hierarchy[target]]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
} else if marray, ok := object.([]interface{}); ok {
|
||||
tmpArray := []interface{}{}
|
||||
for _, val := range marray {
|
||||
tmpGabs := &Container{val}
|
||||
res := tmpGabs.Search(hierarchy[target:]...)
|
||||
if res != nil {
|
||||
tmpArray = append(tmpArray, res.Data())
|
||||
}
|
||||
}
|
||||
if len(tmpArray) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &Container{tmpArray}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return &Container{object}
|
||||
}
|
||||
|
||||
// S - Shorthand method, does the same thing as Search.
|
||||
func (g *Container) S(hierarchy ...string) *Container {
|
||||
return g.Search(hierarchy...)
|
||||
}
|
||||
|
||||
// Exists - Checks whether a path exists.
|
||||
func (g *Container) Exists(hierarchy ...string) bool {
|
||||
return g.Search(hierarchy...) != nil
|
||||
}
|
||||
|
||||
// ExistsP - Checks whether a dot notation path exists.
|
||||
func (g *Container) ExistsP(path string) bool {
|
||||
return g.Exists(strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
// Index - Attempt to find and return an object within a JSON array by index.
|
||||
func (g *Container) Index(index int) *Container {
|
||||
if array, ok := g.Data().([]interface{}); ok {
|
||||
if index >= len(array) {
|
||||
return &Container{nil}
|
||||
}
|
||||
return &Container{array[index]}
|
||||
}
|
||||
return &Container{nil}
|
||||
}
|
||||
|
||||
// Children - Return a slice of all the children of the array. This also works for objects, however,
|
||||
// the children returned for an object will NOT be in order and you lose the names of the returned
|
||||
// objects this way.
|
||||
func (g *Container) Children() ([]*Container, error) {
|
||||
if array, ok := g.Data().([]interface{}); ok {
|
||||
children := make([]*Container, len(array))
|
||||
for i := 0; i < len(array); i++ {
|
||||
children[i] = &Container{array[i]}
|
||||
}
|
||||
return children, nil
|
||||
}
|
||||
if mmap, ok := g.Data().(map[string]interface{}); ok {
|
||||
children := []*Container{}
|
||||
for _, obj := range mmap {
|
||||
children = append(children, &Container{obj})
|
||||
}
|
||||
return children, nil
|
||||
}
|
||||
return nil, ErrNotObjOrArray
|
||||
}
|
||||
|
||||
// ChildrenMap - Return a map of all the children of an object.
|
||||
func (g *Container) ChildrenMap() (map[string]*Container, error) {
|
||||
if mmap, ok := g.Data().(map[string]interface{}); ok {
|
||||
children := map[string]*Container{}
|
||||
for name, obj := range mmap {
|
||||
children[name] = &Container{obj}
|
||||
}
|
||||
return children, nil
|
||||
}
|
||||
return nil, ErrNotObj
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
// Set - Set the value of a field at a JSON path, any parts of the path that do not exist will be
|
||||
// constructed, and if a collision occurs with a non object type whilst iterating the path an error
|
||||
// is returned.
|
||||
func (g *Container) Set(value interface{}, path ...string) (*Container, error) {
|
||||
if len(path) == 0 {
|
||||
g.object = value
|
||||
return g, nil
|
||||
}
|
||||
var object interface{}
|
||||
if g.object == nil {
|
||||
g.object = map[string]interface{}{}
|
||||
}
|
||||
object = g.object
|
||||
for target := 0; target < len(path); target++ {
|
||||
if mmap, ok := object.(map[string]interface{}); ok {
|
||||
if target == len(path)-1 {
|
||||
mmap[path[target]] = value
|
||||
} else if mmap[path[target]] == nil {
|
||||
mmap[path[target]] = map[string]interface{}{}
|
||||
}
|
||||
object = mmap[path[target]]
|
||||
} else {
|
||||
return &Container{nil}, ErrPathCollision
|
||||
}
|
||||
}
|
||||
return &Container{object}, nil
|
||||
}
|
||||
|
||||
// SetP - Does the same as Set, but using a dot notation JSON path.
|
||||
func (g *Container) SetP(value interface{}, path string) (*Container, error) {
|
||||
return g.Set(value, strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
// SetIndex - Set a value of an array element based on the index.
|
||||
func (g *Container) SetIndex(value interface{}, index int) (*Container, error) {
|
||||
if array, ok := g.Data().([]interface{}); ok {
|
||||
if index >= len(array) {
|
||||
return &Container{nil}, ErrOutOfBounds
|
||||
}
|
||||
array[index] = value
|
||||
return &Container{array[index]}, nil
|
||||
}
|
||||
return &Container{nil}, ErrNotArray
|
||||
}
|
||||
|
||||
// Object - Create a new JSON object at a path. Returns an error if the path contains a collision
|
||||
// with a non object type.
|
||||
func (g *Container) Object(path ...string) (*Container, error) {
|
||||
return g.Set(map[string]interface{}{}, path...)
|
||||
}
|
||||
|
||||
// ObjectP - Does the same as Object, but using a dot notation JSON path.
|
||||
func (g *Container) ObjectP(path string) (*Container, error) {
|
||||
return g.Object(strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
// ObjectI - Create a new JSON object at an array index. Returns an error if the object is not an
|
||||
// array or the index is out of bounds.
|
||||
func (g *Container) ObjectI(index int) (*Container, error) {
|
||||
return g.SetIndex(map[string]interface{}{}, index)
|
||||
}
|
||||
|
||||
// Array - Create a new JSON array at a path. Returns an error if the path contains a collision with
|
||||
// a non object type.
|
||||
func (g *Container) Array(path ...string) (*Container, error) {
|
||||
return g.Set([]interface{}{}, path...)
|
||||
}
|
||||
|
||||
// ArrayP - Does the same as Array, but using a dot notation JSON path.
|
||||
func (g *Container) ArrayP(path string) (*Container, error) {
|
||||
return g.Array(strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
// ArrayI - Create a new JSON array at an array index. Returns an error if the object is not an
|
||||
// array or the index is out of bounds.
|
||||
func (g *Container) ArrayI(index int) (*Container, error) {
|
||||
return g.SetIndex([]interface{}{}, index)
|
||||
}
|
||||
|
||||
// ArrayOfSize - Create a new JSON array of a particular size at a path. Returns an error if the
|
||||
// path contains a collision with a non object type.
|
||||
func (g *Container) ArrayOfSize(size int, path ...string) (*Container, error) {
|
||||
a := make([]interface{}, size)
|
||||
return g.Set(a, path...)
|
||||
}
|
||||
|
||||
// ArrayOfSizeP - Does the same as ArrayOfSize, but using a dot notation JSON path.
|
||||
func (g *Container) ArrayOfSizeP(size int, path string) (*Container, error) {
|
||||
return g.ArrayOfSize(size, strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
// ArrayOfSizeI - Create a new JSON array of a particular size at an array index. Returns an error
|
||||
// if the object is not an array or the index is out of bounds.
|
||||
func (g *Container) ArrayOfSizeI(size, index int) (*Container, error) {
|
||||
a := make([]interface{}, size)
|
||||
return g.SetIndex(a, index)
|
||||
}
|
||||
|
||||
// Delete - Delete an element at a JSON path, an error is returned if the element does not exist.
|
||||
func (g *Container) Delete(path ...string) error {
|
||||
var object interface{}
|
||||
|
||||
if g.object == nil {
|
||||
return ErrNotObj
|
||||
}
|
||||
object = g.object
|
||||
for target := 0; target < len(path); target++ {
|
||||
if mmap, ok := object.(map[string]interface{}); ok {
|
||||
if target == len(path)-1 {
|
||||
if _, ok := mmap[path[target]]; ok {
|
||||
delete(mmap, path[target])
|
||||
} else {
|
||||
return ErrNotObj
|
||||
}
|
||||
}
|
||||
object = mmap[path[target]]
|
||||
} else {
|
||||
return ErrNotObj
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteP - Does the same as Delete, but using a dot notation JSON path.
|
||||
func (g *Container) DeleteP(path string) error {
|
||||
return g.Delete(strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
// Merge - Merges two gabs-containers
|
||||
func (g *Container) Merge(toMerge *Container) error {
|
||||
var recursiveFnc func(map[string]interface{}, []string) error
|
||||
recursiveFnc = func(mmap map[string]interface{}, path []string) error {
|
||||
for key, value := range mmap {
|
||||
newPath := append(path, key)
|
||||
if g.Exists(newPath...) {
|
||||
target := g.Search(newPath...)
|
||||
switch t := value.(type) {
|
||||
case map[string]interface{}:
|
||||
switch targetV := target.Data().(type) {
|
||||
case map[string]interface{}:
|
||||
if err := recursiveFnc(t, newPath); err != nil {
|
||||
return err
|
||||
}
|
||||
case []interface{}:
|
||||
g.Set(append(targetV, t), newPath...)
|
||||
default:
|
||||
newSlice := append([]interface{}{}, targetV)
|
||||
g.Set(append(newSlice, t), newPath...)
|
||||
}
|
||||
case []interface{}:
|
||||
for _, valueOfSlice := range t {
|
||||
if err := g.ArrayAppend(valueOfSlice, newPath...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
switch targetV := target.Data().(type) {
|
||||
case []interface{}:
|
||||
g.Set(append(targetV, t), newPath...)
|
||||
default:
|
||||
newSlice := append([]interface{}{}, targetV)
|
||||
g.Set(append(newSlice, t), newPath...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// path doesn't exist. So set the value
|
||||
if _, err := g.Set(value, newPath...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if mmap, ok := toMerge.Data().(map[string]interface{}); ok {
|
||||
return recursiveFnc(mmap, []string{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Array modification/search - Keeping these options simple right now, no need for anything more
|
||||
complicated since you can just cast to []interface{}, modify and then reassign with Set.
|
||||
*/
|
||||
|
||||
// ArrayAppend - Append a value onto a JSON array. If the target is not a JSON array then it will be
|
||||
// converted into one, with its contents as the first element of the array.
|
||||
func (g *Container) ArrayAppend(value interface{}, path ...string) error {
|
||||
if array, ok := g.Search(path...).Data().([]interface{}); ok {
|
||||
array = append(array, value)
|
||||
_, err := g.Set(array, path...)
|
||||
return err
|
||||
}
|
||||
|
||||
newArray := []interface{}{}
|
||||
if d := g.Search(path...).Data(); d != nil {
|
||||
newArray = append(newArray, d)
|
||||
}
|
||||
newArray = append(newArray, value)
|
||||
|
||||
_, err := g.Set(newArray, path...)
|
||||
return err
|
||||
}
|
||||
|
||||
// ArrayAppendP - Append a value onto a JSON array using a dot notation JSON path.
|
||||
func (g *Container) ArrayAppendP(value interface{}, path string) error {
|
||||
return g.ArrayAppend(value, strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
// ArrayRemove - Remove an element from a JSON array.
|
||||
func (g *Container) ArrayRemove(index int, path ...string) error {
|
||||
if index < 0 {
|
||||
return ErrOutOfBounds
|
||||
}
|
||||
array, ok := g.Search(path...).Data().([]interface{})
|
||||
if !ok {
|
||||
return ErrNotArray
|
||||
}
|
||||
if index < len(array) {
|
||||
array = append(array[:index], array[index+1:]...)
|
||||
} else {
|
||||
return ErrOutOfBounds
|
||||
}
|
||||
_, err := g.Set(array, path...)
|
||||
return err
|
||||
}
|
||||
|
||||
// ArrayRemoveP - Remove an element from a JSON array using a dot notation JSON path.
|
||||
func (g *Container) ArrayRemoveP(index int, path string) error {
|
||||
return g.ArrayRemove(index, strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
// ArrayElement - Access an element from a JSON array.
|
||||
func (g *Container) ArrayElement(index int, path ...string) (*Container, error) {
|
||||
if index < 0 {
|
||||
return &Container{nil}, ErrOutOfBounds
|
||||
}
|
||||
array, ok := g.Search(path...).Data().([]interface{})
|
||||
if !ok {
|
||||
return &Container{nil}, ErrNotArray
|
||||
}
|
||||
if index < len(array) {
|
||||
return &Container{array[index]}, nil
|
||||
}
|
||||
return &Container{nil}, ErrOutOfBounds
|
||||
}
|
||||
|
||||
// ArrayElementP - Access an element from a JSON array using a dot notation JSON path.
|
||||
func (g *Container) ArrayElementP(index int, path string) (*Container, error) {
|
||||
return g.ArrayElement(index, strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
// ArrayCount - Count the number of elements in a JSON array.
|
||||
func (g *Container) ArrayCount(path ...string) (int, error) {
|
||||
if array, ok := g.Search(path...).Data().([]interface{}); ok {
|
||||
return len(array), nil
|
||||
}
|
||||
return 0, ErrNotArray
|
||||
}
|
||||
|
||||
// ArrayCountP - Count the number of elements in a JSON array using a dot notation JSON path.
|
||||
func (g *Container) ArrayCountP(path string) (int, error) {
|
||||
return g.ArrayCount(strings.Split(path, ".")...)
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
// Bytes - Converts the contained object back to a JSON []byte blob.
|
||||
func (g *Container) Bytes() []byte {
|
||||
if g.Data() != nil {
|
||||
if bytes, err := json.Marshal(g.object); err == nil {
|
||||
return bytes
|
||||
}
|
||||
}
|
||||
return []byte("{}")
|
||||
}
|
||||
|
||||
// BytesIndent - Converts the contained object to a JSON []byte blob formatted with prefix, indent.
|
||||
func (g *Container) BytesIndent(prefix string, indent string) []byte {
|
||||
if g.object != nil {
|
||||
if bytes, err := json.MarshalIndent(g.object, prefix, indent); err == nil {
|
||||
return bytes
|
||||
}
|
||||
}
|
||||
return []byte("{}")
|
||||
}
|
||||
|
||||
// String - Converts the contained object to a JSON formatted string.
|
||||
func (g *Container) String() string {
|
||||
return string(g.Bytes())
|
||||
}
|
||||
|
||||
// StringIndent - Converts the contained object back to a JSON formatted string with prefix, indent.
|
||||
func (g *Container) StringIndent(prefix string, indent string) string {
|
||||
return string(g.BytesIndent(prefix, indent))
|
||||
}
|
||||
|
||||
// EncodeOpt is a functional option for the EncodeJSON method.
|
||||
type EncodeOpt func(e *json.Encoder)
|
||||
|
||||
// EncodeOptHTMLEscape sets the encoder to escape the JSON for html.
|
||||
func EncodeOptHTMLEscape(doEscape bool) EncodeOpt {
|
||||
return func(e *json.Encoder) {
|
||||
e.SetEscapeHTML(doEscape)
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeOptIndent sets the encoder to indent the JSON output.
|
||||
func EncodeOptIndent(prefix string, indent string) EncodeOpt {
|
||||
return func(e *json.Encoder) {
|
||||
e.SetIndent(prefix, indent)
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeJSON - Encodes the contained object back to a JSON formatted []byte
|
||||
// using a variant list of modifier functions for the encoder being used.
|
||||
// Functions for modifying the output are prefixed with EncodeOpt, e.g.
|
||||
// EncodeOptHTMLEscape.
|
||||
func (g *Container) EncodeJSON(encodeOpts ...EncodeOpt) []byte {
|
||||
var b bytes.Buffer
|
||||
encoder := json.NewEncoder(&b)
|
||||
encoder.SetEscapeHTML(false) // Do not escape by default.
|
||||
for _, opt := range encodeOpts {
|
||||
opt(encoder)
|
||||
}
|
||||
if err := encoder.Encode(g.object); err != nil {
|
||||
return []byte("{}")
|
||||
}
|
||||
result := b.Bytes()
|
||||
if len(result) > 0 {
|
||||
result = result[:len(result)-1]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// New - Create a new gabs JSON object.
|
||||
func New() *Container {
|
||||
return &Container{map[string]interface{}{}}
|
||||
}
|
||||
|
||||
// Consume - Gobble up an already converted JSON object, or a fresh map[string]interface{} object.
|
||||
func Consume(root interface{}) (*Container, error) {
|
||||
return &Container{root}, nil
|
||||
}
|
||||
|
||||
// ParseJSON - Convert a string into a representation of the parsed JSON.
|
||||
func ParseJSON(sample []byte) (*Container, error) {
|
||||
var gabs Container
|
||||
|
||||
if err := json.Unmarshal(sample, &gabs.object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gabs, nil
|
||||
}
|
||||
|
||||
// ParseJSONDecoder - Convert a json.Decoder into a representation of the parsed JSON.
|
||||
func ParseJSONDecoder(decoder *json.Decoder) (*Container, error) {
|
||||
var gabs Container
|
||||
|
||||
if err := decoder.Decode(&gabs.object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gabs, nil
|
||||
}
|
||||
|
||||
// ParseJSONFile - Read a file and convert into a representation of the parsed JSON.
|
||||
func ParseJSONFile(path string) (*Container, error) {
|
||||
if len(path) > 0 {
|
||||
cBytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
container, err := ParseJSON(cBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return container, nil
|
||||
}
|
||||
return nil, ErrInvalidPath
|
||||
}
|
||||
|
||||
// ParseJSONBuffer - Read the contents of a buffer into a representation of the parsed JSON.
|
||||
func ParseJSONBuffer(buffer io.Reader) (*Container, error) {
|
||||
var gabs Container
|
||||
jsonDecoder := json.NewDecoder(buffer)
|
||||
if err := jsonDecoder.Decode(&gabs.object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gabs, nil
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
BIN
vendor/github.com/Jeffail/gabs/gabs_logo.png
generated
vendored
Normal file
BIN
vendor/github.com/Jeffail/gabs/gabs_logo.png
generated
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
41
vendor/github.com/Philipp15b/go-steam/auth.go
generated
vendored
41
vendor/github.com/Philipp15b/go-steam/auth.go
generated
vendored
@@ -2,13 +2,14 @@ package steam
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
. "github.com/Philipp15b/go-steam/protocol"
|
||||
. "github.com/Philipp15b/go-steam/protocol/protobuf"
|
||||
. "github.com/Philipp15b/go-steam/protocol/steamlang"
|
||||
. "github.com/Philipp15b/go-steam/steamid"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Auth struct {
|
||||
@@ -19,23 +20,41 @@ type Auth struct {
|
||||
type SentryHash []byte
|
||||
|
||||
type LogOnDetails struct {
|
||||
Username string
|
||||
Password string
|
||||
AuthCode string
|
||||
Username string
|
||||
|
||||
// If logging into an account without a login key, the account's password.
|
||||
Password string
|
||||
|
||||
// If you have a Steam Guard email code, you can provide it here.
|
||||
AuthCode string
|
||||
|
||||
// If you have a Steam Guard mobile two-factor authentication code, you can provide it here.
|
||||
TwoFactorCode string
|
||||
SentryFileHash SentryHash
|
||||
LoginKey string
|
||||
|
||||
// true if you want to get a login key which can be used in lieu of
|
||||
// a password for subsequent logins. false or omitted otherwise.
|
||||
ShouldRememberPassword bool
|
||||
}
|
||||
|
||||
// Log on with the given details. You must always specify username and
|
||||
// password. For the first login, don't set an authcode or a hash and you'll receive an error
|
||||
// password OR username and loginkey. For the first login, don't set an authcode or a hash and you'll
|
||||
// receive an error (EResult_AccountLogonDenied)
|
||||
// and Steam will send you an authcode. Then you have to login again, this time with the authcode.
|
||||
// Shortly after logging in, you'll receive a MachineAuthUpdateEvent with a hash which allows
|
||||
// you to login without using an authcode in the future.
|
||||
//
|
||||
// If you don't use Steam Guard, username and password are enough.
|
||||
//
|
||||
// After the event EMsg_ClientNewLoginKey is received you can use the LoginKey
|
||||
// to login instead of using the password.
|
||||
func (a *Auth) LogOn(details *LogOnDetails) {
|
||||
if len(details.Username) == 0 || len(details.Password) == 0 {
|
||||
panic("Username and password must be set!")
|
||||
if details.Username == "" {
|
||||
panic("Username must be set!")
|
||||
}
|
||||
if details.Password == "" && details.LoginKey == "" {
|
||||
panic("Password or LoginKey must be set!")
|
||||
}
|
||||
|
||||
logon := new(CMsgClientLogon)
|
||||
@@ -50,6 +69,12 @@ func (a *Auth) LogOn(details *LogOnDetails) {
|
||||
logon.ClientLanguage = proto.String("english")
|
||||
logon.ProtocolVersion = proto.Uint32(MsgClientLogon_CurrentProtocol)
|
||||
logon.ShaSentryfile = details.SentryFileHash
|
||||
if details.LoginKey != "" {
|
||||
logon.LoginKey = proto.String(details.LoginKey)
|
||||
}
|
||||
if details.ShouldRememberPassword {
|
||||
logon.ShouldRememberPassword = proto.Bool(details.ShouldRememberPassword)
|
||||
}
|
||||
|
||||
atomic.StoreUint64(&a.client.steamId, uint64(NewIdAdv(0, 1, int32(EUniverse_Public), int32(EAccountType_Individual))))
|
||||
|
||||
|
||||
3
vendor/github.com/Rhymen/go-whatsapp/.gitignore
generated
vendored
Normal file
3
vendor/github.com/Rhymen/go-whatsapp/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.idea/
|
||||
docs/
|
||||
build/
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015 Dmitri Shuralyov
|
||||
Copyright (c) 2018
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
104
vendor/github.com/Rhymen/go-whatsapp/README.md
generated
vendored
Normal file
104
vendor/github.com/Rhymen/go-whatsapp/README.md
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
# go-whatsapp
|
||||
Package rhymen/go-whatsapp implements the WhatsApp Web API to provide a clean interface for developers. Big thanks to all contributors of the [sigalor/whatsapp-web-reveng](https://github.com/sigalor/whatsapp-web-reveng) project. The official WhatsApp Business API was released in August 2018. You can check it out [here](https://www.whatsapp.com/business/api).
|
||||
|
||||
## Installation
|
||||
```sh
|
||||
go get github.com/Rhymen/go-whatsapp
|
||||
```
|
||||
|
||||
## Usage
|
||||
### Creating a connection
|
||||
```go
|
||||
import (
|
||||
whatsapp "github.com/Rhymen/go-whatsapp"
|
||||
)
|
||||
|
||||
wac, err := whatsapp.NewConn(20 * time.Second)
|
||||
```
|
||||
The duration passed to the NewConn function is used to timeout login requests. If you have a bad internet connection use a higher timeout value. This function only creates a websocket connection, it does not handle authentication.
|
||||
|
||||
### Login
|
||||
```go
|
||||
qrChan := make(chan string)
|
||||
go func() {
|
||||
fmt.Printf("qr code: %v\n", <-qrChan)
|
||||
//show qr code or save it somewhere to scan
|
||||
}
|
||||
sess, err := wac.Login(qrChan)
|
||||
```
|
||||
The authentication process requires you to scan the qr code, that is send through the channel, with the device you are using whatsapp on. The session struct that is returned can be saved and used to restore the login without scanning the qr code again. The qr code has a ttl of 20 seconds and the login function throws a timeout err if the time has passed or any other request fails.
|
||||
|
||||
### Restore
|
||||
```go
|
||||
newSess, err := wac.RestoreWithSession(sess)
|
||||
```
|
||||
The restore function needs a valid session and returns the new session that was created.
|
||||
|
||||
### Add message handlers
|
||||
```go
|
||||
type myHandler struct{}
|
||||
|
||||
func (myHandler) HandleError(err error) {
|
||||
fmt.Fprintf(os.Stderr, "%v", err)
|
||||
}
|
||||
|
||||
func (myHandler) HandleTextMessage(message whatsapp.TextMessage) {
|
||||
fmt.Println(message)
|
||||
}
|
||||
|
||||
func (myHandler) HandleImageMessage(message whatsapp.ImageMessage) {
|
||||
fmt.Println(message)
|
||||
}
|
||||
|
||||
func (myHandler) HandleVideoMessage(message whatsapp.VideoMessage) {
|
||||
fmt.Println(message)
|
||||
}
|
||||
|
||||
func (myHandler) HandleJsonMessage(message string) {
|
||||
fmt.Println(message)
|
||||
}
|
||||
|
||||
wac.AddHandler(myHandler{})
|
||||
```
|
||||
The message handlers are all optional, you don't need to implement anything but the error handler to implement the interface. The ImageMessage and VideoMessage provide a Download function to get the media data.
|
||||
|
||||
### Sending text messages
|
||||
```go
|
||||
text := whatsapp.TextMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: "0123456789@s.whatsapp.net",
|
||||
},
|
||||
Text: "Hello Whatsapp",
|
||||
}
|
||||
|
||||
err := wac.Send(text)
|
||||
```
|
||||
The message will be send over the websocket. The attributes seen above are the required ones. All other relevant attributes (id, timestamp, fromMe, status) are set if they are missing in the struct. For the time being we only support text messages, but other types are planned for the near future.
|
||||
|
||||
## Legal
|
||||
This code is in no way affiliated with, authorized, maintained, sponsored or endorsed by WhatsApp or any of its
|
||||
affiliates or subsidiaries. This is an independent and unofficial software. Use at your own risk.
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
388
vendor/github.com/Rhymen/go-whatsapp/binary/decoder.go
generated
vendored
Normal file
388
vendor/github.com/Rhymen/go-whatsapp/binary/decoder.go
generated
vendored
Normal file
@@ -0,0 +1,388 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Rhymen/go-whatsapp/binary/token"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type binaryDecoder struct {
|
||||
data []byte
|
||||
index int
|
||||
}
|
||||
|
||||
func NewDecoder(data []byte) *binaryDecoder {
|
||||
return &binaryDecoder{data, 0}
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) checkEOS(length int) error {
|
||||
if r.index+length > len(r.data) {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readByte() (byte, error) {
|
||||
if err := r.checkEOS(1); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
b := r.data[r.index]
|
||||
r.index++
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readIntN(n int, littleEndian bool) (int, error) {
|
||||
if err := r.checkEOS(n); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var ret int
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
var curShift int
|
||||
if littleEndian {
|
||||
curShift = i
|
||||
} else {
|
||||
curShift = n - i - 1
|
||||
}
|
||||
ret |= int(r.data[r.index+i]) << uint(curShift*8)
|
||||
}
|
||||
|
||||
r.index += n
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readInt8(littleEndian bool) (int, error) {
|
||||
return r.readIntN(1, littleEndian)
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readInt16(littleEndian bool) (int, error) {
|
||||
return r.readIntN(2, littleEndian)
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readInt20() (int, error) {
|
||||
if err := r.checkEOS(3); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ret := ((int(r.data[r.index]) & 15) << 16) + (int(r.data[r.index+1]) << 8) + int(r.data[r.index+2])
|
||||
r.index += 3
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readInt32(littleEndian bool) (int, error) {
|
||||
return r.readIntN(4, littleEndian)
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readInt64(littleEndian bool) (int, error) {
|
||||
return r.readIntN(8, littleEndian)
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readPacked8(tag int) (string, error) {
|
||||
startByte, err := r.readByte()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ret := ""
|
||||
|
||||
for i := 0; i < int(startByte&127); i++ {
|
||||
currByte, err := r.readByte()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lower, err := unpackByte(tag, currByte&0xF0>>4)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
upper, err := unpackByte(tag, currByte&0x0F)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ret += lower + upper
|
||||
}
|
||||
|
||||
if startByte>>7 != 0 {
|
||||
ret = ret[:len(ret)-1]
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func unpackByte(tag int, value byte) (string, error) {
|
||||
switch tag {
|
||||
case token.NIBBLE_8:
|
||||
return unpackNibble(value)
|
||||
case token.HEX_8:
|
||||
return unpackHex(value)
|
||||
default:
|
||||
return "", fmt.Errorf("unpackByte with unknown tag %d", tag)
|
||||
}
|
||||
}
|
||||
|
||||
func unpackNibble(value byte) (string, error) {
|
||||
switch {
|
||||
case value < 0 || value > 15:
|
||||
return "", fmt.Errorf("unpackNibble with value %d", value)
|
||||
case value == 10:
|
||||
return "-", nil
|
||||
case value == 11:
|
||||
return ".", nil
|
||||
case value == 15:
|
||||
return "\x00", nil
|
||||
default:
|
||||
return strconv.Itoa(int(value)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func unpackHex(value byte) (string, error) {
|
||||
switch {
|
||||
case value < 0 || value > 15:
|
||||
return "", fmt.Errorf("unpackHex with value %d", value)
|
||||
case value < 10:
|
||||
return strconv.Itoa(int(value)), nil
|
||||
default:
|
||||
return string('A' + value - 10), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readListSize(tag int) (int, error) {
|
||||
switch tag {
|
||||
case token.LIST_EMPTY:
|
||||
return 0, nil
|
||||
case token.LIST_8:
|
||||
return r.readInt8(false)
|
||||
case token.LIST_16:
|
||||
return r.readInt16(false)
|
||||
default:
|
||||
return 0, fmt.Errorf("readListSize with unknown tag %d at position %d", tag, r.index)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readString(tag int) (string, error) {
|
||||
switch {
|
||||
case tag >= 3 && tag <= len(token.SingleByteTokens):
|
||||
tok, err := token.GetSingleToken(tag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if tok == "s.whatsapp.net" {
|
||||
tok = "c.us"
|
||||
}
|
||||
|
||||
return tok, nil
|
||||
case tag == token.DICTIONARY_0 || tag == token.DICTIONARY_1 || tag == token.DICTIONARY_2 || tag == token.DICTIONARY_3:
|
||||
i, err := r.readInt8(false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token.GetDoubleToken(tag-token.DICTIONARY_0, i)
|
||||
case tag == token.LIST_EMPTY:
|
||||
return "", nil
|
||||
case tag == token.BINARY_8:
|
||||
length, err := r.readInt8(false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return r.readStringFromChars(length)
|
||||
case tag == token.BINARY_20:
|
||||
length, err := r.readInt20()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return r.readStringFromChars(length)
|
||||
case tag == token.BINARY_32:
|
||||
length, err := r.readInt32(false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return r.readStringFromChars(length)
|
||||
case tag == token.JID_PAIR:
|
||||
b, err := r.readByte()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
i, err := r.readString(int(b))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b, err = r.readByte()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
j, err := r.readString(int(b))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if i == "" || j == "" {
|
||||
return "", fmt.Errorf("invalid jid pair: %s - %s", i, j)
|
||||
}
|
||||
|
||||
return i + "@" + j, nil
|
||||
case tag == token.NIBBLE_8 || tag == token.HEX_8:
|
||||
return r.readPacked8(tag)
|
||||
default:
|
||||
return "", fmt.Errorf("invalid string with tag %d", tag)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readStringFromChars(length int) (string, error) {
|
||||
if err := r.checkEOS(length); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ret := r.data[r.index : r.index+length]
|
||||
r.index += length
|
||||
|
||||
return string(ret), nil
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readAttributes(n int) (map[string]string, error) {
|
||||
if n == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ret := make(map[string]string)
|
||||
for i := 0; i < n; i++ {
|
||||
idx, err := r.readInt8(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
index, err := r.readString(idx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idx, err = r.readInt8(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret[index], err = r.readString(idx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readList(tag int) ([]Node, error) {
|
||||
size, err := r.readListSize(tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]Node, size)
|
||||
for i := 0; i < size; i++ {
|
||||
n, err := r.ReadNode()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret[i] = *n
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) ReadNode() (*Node, error) {
|
||||
ret := &Node{}
|
||||
|
||||
size, err := r.readInt8(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listSize, err := r.readListSize(size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
descrTag, err := r.readInt8(false)
|
||||
if descrTag == token.STREAM_END {
|
||||
return nil, fmt.Errorf("unexpected stream end")
|
||||
}
|
||||
ret.Description, err = r.readString(descrTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if listSize == 0 || ret.Description == "" {
|
||||
return nil, fmt.Errorf("invalid Node")
|
||||
}
|
||||
|
||||
ret.Attributes, err = r.readAttributes((listSize - 1) >> 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if listSize%2 == 1 {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
tag, err := r.readInt8(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case token.LIST_EMPTY, token.LIST_8, token.LIST_16:
|
||||
ret.Content, err = r.readList(tag)
|
||||
case token.BINARY_8:
|
||||
size, err = r.readInt8(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret.Content, err = r.readBytes(size)
|
||||
case token.BINARY_20:
|
||||
size, err = r.readInt20()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret.Content, err = r.readBytes(size)
|
||||
case token.BINARY_32:
|
||||
size, err = r.readInt32(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret.Content, err = r.readBytes(size)
|
||||
default:
|
||||
ret.Content, err = r.readString(tag)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *binaryDecoder) readBytes(n int) ([]byte, error) {
|
||||
ret := make([]byte, n)
|
||||
var err error
|
||||
|
||||
for i := range ret {
|
||||
ret[i], err = r.readByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
351
vendor/github.com/Rhymen/go-whatsapp/binary/encoder.go
generated
vendored
Normal file
351
vendor/github.com/Rhymen/go-whatsapp/binary/encoder.go
generated
vendored
Normal file
@@ -0,0 +1,351 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Rhymen/go-whatsapp/binary/token"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type binaryEncoder struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func NewEncoder() *binaryEncoder {
|
||||
return &binaryEncoder{make([]byte, 0)}
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) GetData() []byte {
|
||||
return w.data
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) pushByte(b byte) {
|
||||
w.data = append(w.data, b)
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) pushBytes(bytes []byte) {
|
||||
w.data = append(w.data, bytes...)
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) pushIntN(value, n int, littleEndian bool) {
|
||||
for i := 0; i < n; i++ {
|
||||
var curShift int
|
||||
if littleEndian {
|
||||
curShift = i
|
||||
} else {
|
||||
curShift = n - i - 1
|
||||
}
|
||||
w.pushByte(byte((value >> uint(curShift*8)) & 0xFF))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) pushInt20(value int) {
|
||||
w.pushBytes([]byte{byte((value >> 16) & 0x0F), byte((value >> 8) & 0xFF), byte(value & 0xFF)})
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) pushInt8(value int) {
|
||||
w.pushIntN(value, 1, false)
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) pushInt16(value int) {
|
||||
w.pushIntN(value, 2, false)
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) pushInt32(value int) {
|
||||
w.pushIntN(value, 4, false)
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) pushInt64(value int) {
|
||||
w.pushIntN(value, 8, false)
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) pushString(value string) {
|
||||
w.pushBytes([]byte(value))
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) writeByteLength(length int) error {
|
||||
if length > math.MaxInt32 {
|
||||
return fmt.Errorf("length is too large: %d", length)
|
||||
} else if length >= (1 << 20) {
|
||||
w.pushByte(token.BINARY_32)
|
||||
w.pushInt32(length)
|
||||
} else if length >= 256 {
|
||||
w.pushByte(token.BINARY_20)
|
||||
w.pushInt20(length)
|
||||
} else {
|
||||
w.pushByte(token.BINARY_8)
|
||||
w.pushInt8(length)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) WriteNode(n Node) error {
|
||||
numAttributes := 0
|
||||
if n.Attributes != nil {
|
||||
numAttributes = len(n.Attributes)
|
||||
}
|
||||
|
||||
hasContent := 0
|
||||
if n.Content != nil {
|
||||
hasContent = 1
|
||||
}
|
||||
|
||||
w.writeListStart(2*numAttributes + 1 + hasContent)
|
||||
if err := w.writeString(n.Description, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := w.writeAttributes(n.Attributes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := w.writeChildren(n.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) writeString(tok string, i bool) error {
|
||||
if !i && tok == "c.us" {
|
||||
if err := w.writeToken(token.IndexOfSingleToken("s.whatsapp.net")); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
tokenIndex := token.IndexOfSingleToken(tok)
|
||||
if tokenIndex == -1 {
|
||||
jidSepIndex := strings.Index(tok, "@")
|
||||
if jidSepIndex < 1 {
|
||||
w.writeStringRaw(tok)
|
||||
} else {
|
||||
w.writeJid(tok[:jidSepIndex], tok[jidSepIndex+1:])
|
||||
}
|
||||
} else {
|
||||
if tokenIndex < token.SINGLE_BYTE_MAX {
|
||||
if err := w.writeToken(tokenIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
singleByteOverflow := tokenIndex - token.SINGLE_BYTE_MAX
|
||||
dictionaryIndex := singleByteOverflow >> 8
|
||||
if dictionaryIndex < 0 || dictionaryIndex > 3 {
|
||||
return fmt.Errorf("double byte dictionary token out of range: %v", tok)
|
||||
}
|
||||
if err := w.writeToken(token.DICTIONARY_0 + dictionaryIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.writeToken(singleByteOverflow % 256); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) writeStringRaw(value string) error {
|
||||
if err := w.writeByteLength(len(value)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.pushString(value)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) writeJid(jidLeft, jidRight string) error {
|
||||
w.pushByte(token.JID_PAIR)
|
||||
|
||||
if jidLeft != "" {
|
||||
if err := w.writePackedBytes(jidLeft); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := w.writeToken(token.LIST_EMPTY); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.writeString(jidRight, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) writeToken(tok int) error {
|
||||
if tok < len(token.SingleByteTokens) {
|
||||
w.pushByte(byte(tok))
|
||||
} else if tok <= 500 {
|
||||
return fmt.Errorf("invalid token: %d", tok)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) writeAttributes(attributes map[string]string) error {
|
||||
if attributes == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for key, val := range attributes {
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := w.writeString(key, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := w.writeString(val, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) writeChildren(children interface{}) error {
|
||||
if children == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch childs := children.(type) {
|
||||
case string:
|
||||
if err := w.writeString(childs, true); err != nil {
|
||||
return err
|
||||
}
|
||||
case []byte:
|
||||
if err := w.writeByteLength(len(childs)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.pushBytes(childs)
|
||||
case []Node:
|
||||
w.writeListStart(len(childs))
|
||||
for _, n := range childs {
|
||||
if err := w.WriteNode(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("cannot write child of type: %T", children)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) writeListStart(listSize int) {
|
||||
if listSize == 0 {
|
||||
w.pushByte(byte(token.LIST_EMPTY))
|
||||
} else if listSize < 256 {
|
||||
w.pushByte(byte(token.LIST_8))
|
||||
w.pushInt8(listSize)
|
||||
} else {
|
||||
w.pushByte(byte(token.LIST_16))
|
||||
w.pushInt16(listSize)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) writePackedBytes(value string) error {
|
||||
if err := w.writePackedBytesImpl(value, token.NIBBLE_8); err != nil {
|
||||
if err := w.writePackedBytesImpl(value, token.HEX_8); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) writePackedBytesImpl(value string, dataType int) error {
|
||||
numBytes := len(value)
|
||||
if numBytes > token.PACKED_MAX {
|
||||
return fmt.Errorf("too many bytes to pack: %d", numBytes)
|
||||
}
|
||||
|
||||
w.pushByte(byte(dataType))
|
||||
|
||||
x := 0
|
||||
if numBytes%2 != 0 {
|
||||
x = 128
|
||||
}
|
||||
w.pushByte(byte(x | int(math.Ceil(float64(numBytes)/2.0))))
|
||||
for i, l := 0, numBytes/2; i < l; i++ {
|
||||
b, err := w.packBytePair(dataType, value[2*i:2*i+1], value[2*i+1:2*i+2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.pushByte(byte(b))
|
||||
}
|
||||
|
||||
if (numBytes % 2) != 0 {
|
||||
b, err := w.packBytePair(dataType, value[numBytes-1:], "\x00")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.pushByte(byte(b))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *binaryEncoder) packBytePair(packType int, part1, part2 string) (int, error) {
|
||||
if packType == token.NIBBLE_8 {
|
||||
n1, err := packNibble(part1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n2, err := packNibble(part2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return (n1 << 4) | n2, nil
|
||||
} else if packType == token.HEX_8 {
|
||||
n1, err := packHex(part1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n2, err := packHex(part2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return (n1 << 4) | n2, nil
|
||||
} else {
|
||||
return 0, fmt.Errorf("invalid pack type (%d) for byte pair: %s / %s", packType, part1, part2)
|
||||
}
|
||||
}
|
||||
|
||||
func packNibble(value string) (int, error) {
|
||||
if value >= "0" && value <= "9" {
|
||||
return strconv.Atoi(value)
|
||||
} else if value == "-" {
|
||||
return 10, nil
|
||||
} else if value == "." {
|
||||
return 11, nil
|
||||
} else if value == "\x00" {
|
||||
return 15, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("invalid string to pack as nibble: %v", value)
|
||||
}
|
||||
|
||||
func packHex(value string) (int, error) {
|
||||
if (value >= "0" && value <= "9") || (value >= "A" && value <= "F") || (value >= "a" && value <= "f") {
|
||||
d, err := strconv.ParseInt(value, 16, 0)
|
||||
return int(d), err
|
||||
} else if value == "\x00" {
|
||||
return 15, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("invalid string to pack as hex: %v", value)
|
||||
}
|
||||
103
vendor/github.com/Rhymen/go-whatsapp/binary/node.go
generated
vendored
Normal file
103
vendor/github.com/Rhymen/go-whatsapp/binary/node.go
generated
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
pb "github.com/Rhymen/go-whatsapp/binary/proto"
|
||||
"github.com/golang/protobuf/proto"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
Description string
|
||||
Attributes map[string]string
|
||||
Content interface{}
|
||||
}
|
||||
|
||||
func Marshal(n Node) ([]byte, error) {
|
||||
if n.Attributes != nil && n.Content != nil {
|
||||
a, err := marshalMessageArray(n.Content.([]interface{}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n.Content = a
|
||||
}
|
||||
|
||||
w := NewEncoder()
|
||||
if err := w.WriteNode(n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w.GetData(), nil
|
||||
}
|
||||
|
||||
func marshalMessageArray(messages []interface{}) ([]Node, error) {
|
||||
ret := make([]Node, len(messages))
|
||||
|
||||
for i, m := range messages {
|
||||
if wmi, ok := m.(*pb.WebMessageInfo); ok {
|
||||
b, err := marshalWebMessageInfo(wmi)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
ret[i] = Node{"message", nil, b}
|
||||
} else {
|
||||
ret[i], ok = m.(Node)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid Node")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func marshalWebMessageInfo(p *pb.WebMessageInfo) ([]byte, error) {
|
||||
b, err := proto.Marshal(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func Unmarshal(data []byte) (*Node, error) {
|
||||
r := NewDecoder(data)
|
||||
n, err := r.ReadNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if n != nil && n.Attributes != nil && n.Content != nil {
|
||||
n.Content, err = unmarshalMessageArray(n.Content.([]Node))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func unmarshalMessageArray(messages []Node) ([]interface{}, error) {
|
||||
ret := make([]interface{}, len(messages))
|
||||
|
||||
for i, msg := range messages {
|
||||
if msg.Description == "message" {
|
||||
info, err := unmarshalWebMessageInfo(msg.Content.([]byte))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[i] = info
|
||||
} else {
|
||||
ret[i] = msg
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func unmarshalWebMessageInfo(msg []byte) (*pb.WebMessageInfo, error) {
|
||||
message := &pb.WebMessageInfo{}
|
||||
err := proto.Unmarshal(msg, message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return message, nil
|
||||
}
|
||||
3800
vendor/github.com/Rhymen/go-whatsapp/binary/proto/def.pb.go
generated
vendored
Normal file
3800
vendor/github.com/Rhymen/go-whatsapp/binary/proto/def.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
417
vendor/github.com/Rhymen/go-whatsapp/binary/proto/def.proto
generated
vendored
Normal file
417
vendor/github.com/Rhymen/go-whatsapp/binary/proto/def.proto
generated
vendored
Normal file
@@ -0,0 +1,417 @@
|
||||
syntax = "proto2";
|
||||
package proto;
|
||||
|
||||
message FingerprintData {
|
||||
optional string publicKey = 1;
|
||||
optional string identifier = 2;
|
||||
}
|
||||
|
||||
message CombinedFingerprint {
|
||||
optional uint32 version = 1;
|
||||
optional FingerprintData localFingerprint = 2;
|
||||
optional FingerprintData remoteFingerprint = 3;
|
||||
}
|
||||
|
||||
message MessageKey {
|
||||
optional string remoteJid = 1;
|
||||
optional bool fromMe = 2;
|
||||
optional string id = 3;
|
||||
optional string participant = 4;
|
||||
}
|
||||
|
||||
message SenderKeyDistributionMessage {
|
||||
optional string groupId = 1;
|
||||
optional bytes axolotlSenderKeyDistributionMessage = 2;
|
||||
}
|
||||
|
||||
message ImageMessage {
|
||||
optional string url = 1;
|
||||
optional string mimetype = 2;
|
||||
optional string caption = 3;
|
||||
optional bytes fileSha256 = 4;
|
||||
optional uint64 fileLength = 5;
|
||||
optional uint32 height = 6;
|
||||
optional uint32 width = 7;
|
||||
optional bytes mediaKey = 8;
|
||||
optional bytes fileEncSha256 = 9;
|
||||
repeated InteractiveAnnotation interactiveAnnotations = 10;
|
||||
optional string directPath = 11;
|
||||
optional bytes jpegThumbnail = 16;
|
||||
optional ContextInfo contextInfo = 17;
|
||||
optional bytes firstScanSidecar = 18;
|
||||
optional uint32 firstScanLength = 19;
|
||||
}
|
||||
|
||||
message ContactMessage {
|
||||
optional string displayName = 1;
|
||||
optional string vcard = 16;
|
||||
optional ContextInfo contextInfo = 17;
|
||||
}
|
||||
|
||||
message LocationMessage {
|
||||
optional double degreesLatitude = 1;
|
||||
optional double degreesLongitude = 2;
|
||||
optional string name = 3;
|
||||
optional string address = 4;
|
||||
optional string url = 5;
|
||||
optional bytes jpegThumbnail = 16;
|
||||
optional ContextInfo contextInfo = 17;
|
||||
}
|
||||
|
||||
message ExtendedTextMessage {
|
||||
optional string text = 1;
|
||||
optional string matchedText = 2;
|
||||
optional string canonicalUrl = 4;
|
||||
optional string description = 5;
|
||||
optional string title = 6;
|
||||
optional fixed32 textArgb = 7;
|
||||
optional fixed32 backgroundArgb = 8;
|
||||
enum FONTTYPE {
|
||||
SANS_SERIF = 0;
|
||||
SERIF = 1;
|
||||
NORICAN_REGULAR = 2;
|
||||
BRYNDAN_WRITE = 3;
|
||||
BEBASNEUE_REGULAR = 4;
|
||||
OSWALD_HEAVY = 5;
|
||||
}
|
||||
optional FONTTYPE font = 9;
|
||||
optional bytes jpegThumbnail = 16;
|
||||
optional ContextInfo contextInfo = 17;
|
||||
}
|
||||
|
||||
message DocumentMessage {
|
||||
optional string url = 1;
|
||||
optional string mimetype = 2;
|
||||
optional string title = 3;
|
||||
optional bytes fileSha256 = 4;
|
||||
optional uint64 fileLength = 5;
|
||||
optional uint32 pageCount = 6;
|
||||
optional bytes mediaKey = 7;
|
||||
optional string fileName = 8;
|
||||
optional bytes fileEncSha256 = 9;
|
||||
optional string directPath = 10;
|
||||
optional bytes jpegThumbnail = 16;
|
||||
optional ContextInfo contextInfo = 17;
|
||||
}
|
||||
|
||||
message AudioMessage {
|
||||
optional string url = 1;
|
||||
optional string mimetype = 2;
|
||||
optional bytes fileSha256 = 3;
|
||||
optional uint64 fileLength = 4;
|
||||
optional uint32 seconds = 5;
|
||||
optional bool ptt = 6;
|
||||
optional bytes mediaKey = 7;
|
||||
optional bytes fileEncSha256 = 8;
|
||||
optional string directPath = 9;
|
||||
optional ContextInfo contextInfo = 17;
|
||||
optional bytes streamingSidecar = 18;
|
||||
}
|
||||
|
||||
message VideoMessage {
|
||||
optional string url = 1;
|
||||
optional string mimetype = 2;
|
||||
optional bytes fileSha256 = 3;
|
||||
optional uint64 fileLength = 4;
|
||||
optional uint32 seconds = 5;
|
||||
optional bytes mediaKey = 6;
|
||||
optional string caption = 7;
|
||||
optional bool gifPlayback = 8;
|
||||
optional uint32 height = 9;
|
||||
optional uint32 width = 10;
|
||||
optional bytes fileEncSha256 = 11;
|
||||
repeated InteractiveAnnotation interactiveAnnotations = 12;
|
||||
optional string directPath = 13;
|
||||
optional bytes jpegThumbnail = 16;
|
||||
optional ContextInfo contextInfo = 17;
|
||||
optional bytes streamingSidecar = 18;
|
||||
enum ATTRIBUTION {
|
||||
NONE = 0;
|
||||
GIPHY = 1;
|
||||
TENOR = 2;
|
||||
}
|
||||
optional ATTRIBUTION gifAttribution = 19;
|
||||
}
|
||||
|
||||
message Call {
|
||||
optional bytes callKey = 1;
|
||||
}
|
||||
|
||||
message Chat {
|
||||
optional string displayName = 1;
|
||||
optional string id = 2;
|
||||
}
|
||||
|
||||
message ProtocolMessage {
|
||||
optional MessageKey key = 1;
|
||||
enum TYPE {
|
||||
REVOKE = 0;
|
||||
}
|
||||
optional TYPE type = 2;
|
||||
}
|
||||
|
||||
message ContactsArrayMessage {
|
||||
optional string displayName = 1;
|
||||
repeated ContactMessage contacts = 2;
|
||||
optional ContextInfo contextInfo = 17;
|
||||
}
|
||||
|
||||
message HSMCurrency {
|
||||
optional string currencyCode = 1;
|
||||
optional int64 amount1000 = 2;
|
||||
}
|
||||
|
||||
message HSMDateTimeComponent {
|
||||
enum DAYOFWEEKTYPE {
|
||||
MONDAY = 1;
|
||||
TUESDAY = 2;
|
||||
WEDNESDAY = 3;
|
||||
THURSDAY = 4;
|
||||
FRIDAY = 5;
|
||||
SATURDAY = 6;
|
||||
SUNDAY = 7;
|
||||
}
|
||||
optional DAYOFWEEKTYPE dayOfWeek = 1;
|
||||
optional uint32 year = 2;
|
||||
optional uint32 month = 3;
|
||||
optional uint32 dayOfMonth = 4;
|
||||
optional uint32 hour = 5;
|
||||
optional uint32 minute = 6;
|
||||
enum CALENDARTYPE {
|
||||
GREGORIAN = 1;
|
||||
SOLAR_HIJRI = 2;
|
||||
}
|
||||
optional CALENDARTYPE calendar = 7;
|
||||
}
|
||||
|
||||
message HSMDateTimeUnixEpoch {
|
||||
optional int64 timestamp = 1;
|
||||
}
|
||||
|
||||
message HSMDateTime {
|
||||
oneof datetimeOneof {
|
||||
HSMDateTimeComponent component = 1;
|
||||
HSMDateTimeUnixEpoch unixEpoch = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message HSMLocalizableParameter {
|
||||
optional string default = 1;
|
||||
oneof paramOneof {
|
||||
HSMCurrency currency = 2;
|
||||
HSMDateTime dateTime = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message HighlyStructuredMessage {
|
||||
optional string namespace = 1;
|
||||
optional string elementName = 2;
|
||||
repeated string params = 3;
|
||||
optional string fallbackLg = 4;
|
||||
optional string fallbackLc = 5;
|
||||
repeated HSMLocalizableParameter localizableParams = 6;
|
||||
}
|
||||
|
||||
message SendPaymentMessage {
|
||||
optional Message noteMessage = 2;
|
||||
}
|
||||
|
||||
message RequestPaymentMessage {
|
||||
optional string currencyCodeIso4217 = 1;
|
||||
optional uint64 amount1000 = 2;
|
||||
optional string requestFrom = 3;
|
||||
optional Message noteMessage = 4;
|
||||
}
|
||||
|
||||
message LiveLocationMessage {
|
||||
optional double degreesLatitude = 1;
|
||||
optional double degreesLongitude = 2;
|
||||
optional uint32 accuracyInMeters = 3;
|
||||
optional float speedInMps = 4;
|
||||
optional uint32 degreesClockwiseFromMagneticNorth = 5;
|
||||
optional string caption = 6;
|
||||
optional int64 sequenceNumber = 7;
|
||||
optional bytes jpegThumbnail = 16;
|
||||
optional ContextInfo contextInfo = 17;
|
||||
}
|
||||
|
||||
message StickerMessage {
|
||||
optional string url = 1;
|
||||
optional bytes fileSha256 = 2;
|
||||
optional bytes fileEncSha256 = 3;
|
||||
optional bytes mediaKey = 4;
|
||||
optional string mimetype = 5;
|
||||
optional uint32 height = 6;
|
||||
optional uint32 width = 7;
|
||||
optional string directPath = 8;
|
||||
optional uint64 fileLength = 9;
|
||||
optional bytes pngThumbnail = 16;
|
||||
optional ContextInfo contextInfo = 17;
|
||||
}
|
||||
|
||||
message Message {
|
||||
optional string conversation = 1;
|
||||
optional SenderKeyDistributionMessage senderKeyDistributionMessage = 2;
|
||||
optional ImageMessage imageMessage = 3;
|
||||
optional ContactMessage contactMessage = 4;
|
||||
optional LocationMessage locationMessage = 5;
|
||||
optional ExtendedTextMessage extendedTextMessage = 6;
|
||||
optional DocumentMessage documentMessage = 7;
|
||||
optional AudioMessage audioMessage = 8;
|
||||
optional VideoMessage videoMessage = 9;
|
||||
optional Call call = 10;
|
||||
optional Chat chat = 11;
|
||||
optional ProtocolMessage protocolMessage = 12;
|
||||
optional ContactsArrayMessage contactsArrayMessage = 13;
|
||||
optional HighlyStructuredMessage highlyStructuredMessage = 14;
|
||||
optional SenderKeyDistributionMessage fastRatchetKeySenderKeyDistributionMessage = 15;
|
||||
optional SendPaymentMessage sendPaymentMessage = 16;
|
||||
optional RequestPaymentMessage requestPaymentMessage = 17;
|
||||
optional LiveLocationMessage liveLocationMessage = 18;
|
||||
optional StickerMessage stickerMessage = 20;
|
||||
}
|
||||
|
||||
message ContextInfo {
|
||||
optional string stanzaId = 1;
|
||||
optional string participant = 2;
|
||||
repeated Message quotedMessage = 3;
|
||||
optional string remoteJid = 4;
|
||||
repeated string mentionedJid = 15;
|
||||
optional string conversionSource = 18;
|
||||
optional bytes conversionData = 19;
|
||||
optional uint32 conversionDelaySeconds = 20;
|
||||
optional bool isForwarded = 22;
|
||||
reserved 16, 17;
|
||||
}
|
||||
|
||||
message InteractiveAnnotation {
|
||||
repeated Point polygonVertices = 1;
|
||||
oneof action {
|
||||
Location location = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message Point {
|
||||
optional double x = 3;
|
||||
optional double y = 4;
|
||||
}
|
||||
|
||||
message Location {
|
||||
optional double degreesLatitude = 1;
|
||||
optional double degreesLongitude = 2;
|
||||
optional string name = 3;
|
||||
}
|
||||
|
||||
message WebMessageInfo {
|
||||
required MessageKey key = 1;
|
||||
optional Message message = 2;
|
||||
optional uint64 messageTimestamp = 3;
|
||||
enum STATUS {
|
||||
ERROR = 0;
|
||||
PENDING = 1;
|
||||
SERVER_ACK = 2;
|
||||
DELIVERY_ACK = 3;
|
||||
READ = 4;
|
||||
PLAYED = 5;
|
||||
}
|
||||
optional STATUS status = 4 [default=PENDING];
|
||||
optional string participant = 5;
|
||||
optional bool ignore = 16;
|
||||
optional bool starred = 17;
|
||||
optional bool broadcast = 18;
|
||||
optional string pushName = 19;
|
||||
optional bytes mediaCiphertextSha256 = 20;
|
||||
optional bool multicast = 21;
|
||||
optional bool urlText = 22;
|
||||
optional bool urlNumber = 23;
|
||||
enum STUBTYPE {
|
||||
UNKNOWN = 0;
|
||||
REVOKE = 1;
|
||||
CIPHERTEXT = 2;
|
||||
FUTUREPROOF = 3;
|
||||
NON_VERIFIED_TRANSITION = 4;
|
||||
UNVERIFIED_TRANSITION = 5;
|
||||
VERIFIED_TRANSITION = 6;
|
||||
VERIFIED_LOW_UNKNOWN = 7;
|
||||
VERIFIED_HIGH = 8;
|
||||
VERIFIED_INITIAL_UNKNOWN = 9;
|
||||
VERIFIED_INITIAL_LOW = 10;
|
||||
VERIFIED_INITIAL_HIGH = 11;
|
||||
VERIFIED_TRANSITION_ANY_TO_NONE = 12;
|
||||
VERIFIED_TRANSITION_ANY_TO_HIGH = 13;
|
||||
VERIFIED_TRANSITION_HIGH_TO_LOW = 14;
|
||||
VERIFIED_TRANSITION_HIGH_TO_UNKNOWN = 15;
|
||||
VERIFIED_TRANSITION_UNKNOWN_TO_LOW = 16;
|
||||
VERIFIED_TRANSITION_LOW_TO_UNKNOWN = 17;
|
||||
VERIFIED_TRANSITION_NONE_TO_LOW = 18;
|
||||
VERIFIED_TRANSITION_NONE_TO_UNKNOWN = 19;
|
||||
GROUP_CREATE = 20;
|
||||
GROUP_CHANGE_SUBJECT = 21;
|
||||
GROUP_CHANGE_ICON = 22;
|
||||
GROUP_CHANGE_INVITE_LINK = 23;
|
||||
GROUP_CHANGE_DESCRIPTION = 24;
|
||||
GROUP_CHANGE_RESTRICT = 25;
|
||||
GROUP_CHANGE_ANNOUNCE = 26;
|
||||
GROUP_PARTICIPANT_ADD = 27;
|
||||
GROUP_PARTICIPANT_REMOVE = 28;
|
||||
GROUP_PARTICIPANT_PROMOTE = 29;
|
||||
GROUP_PARTICIPANT_DEMOTE = 30;
|
||||
GROUP_PARTICIPANT_INVITE = 31;
|
||||
GROUP_PARTICIPANT_LEAVE = 32;
|
||||
GROUP_PARTICIPANT_CHANGE_NUMBER = 33;
|
||||
BROADCAST_CREATE = 34;
|
||||
BROADCAST_ADD = 35;
|
||||
BROADCAST_REMOVE = 36;
|
||||
GENERIC_NOTIFICATION = 37;
|
||||
E2E_IDENTITY_CHANGED = 38;
|
||||
E2E_ENCRYPTED = 39;
|
||||
CALL_MISSED_VOICE = 40;
|
||||
CALL_MISSED_VIDEO = 41;
|
||||
INDIVIDUAL_CHANGE_NUMBER = 42;
|
||||
GROUP_DELETE = 43;
|
||||
}
|
||||
optional STUBTYPE messageStubType = 24;
|
||||
optional bool clearMedia = 25;
|
||||
repeated string messageStubParameters = 26;
|
||||
optional uint32 duration = 27;
|
||||
repeated string labels = 28;
|
||||
}
|
||||
|
||||
message WebNotificationsInfo {
|
||||
optional uint64 timestamp = 2;
|
||||
optional uint32 unreadChats = 3;
|
||||
optional uint32 notifyMessageCount = 4;
|
||||
repeated Message notifyMessages = 5;
|
||||
}
|
||||
|
||||
message NotificationMessageInfo {
|
||||
optional MessageKey key = 1;
|
||||
optional Message message = 2;
|
||||
optional uint64 messageTimestamp = 3;
|
||||
optional string participant = 4;
|
||||
}
|
||||
|
||||
message TabletNotificationsInfo {
|
||||
optional uint64 timestamp = 2;
|
||||
optional uint32 unreadChats = 3;
|
||||
optional uint32 notifyMessageCount = 4;
|
||||
repeated Message notifyMessage = 5;
|
||||
}
|
||||
|
||||
message WebFeatures {
|
||||
enum FLAG {
|
||||
NOT_IMPLEMENTED = 0;
|
||||
IMPLEMENTED = 1;
|
||||
OPTIONAL = 2;
|
||||
}
|
||||
optional FLAG labelsDisplay = 1;
|
||||
optional FLAG voipIndividualOutgoing = 2;
|
||||
optional FLAG groupsV3 = 3;
|
||||
optional FLAG groupsV3Create = 4;
|
||||
optional FLAG changeNumberV2 = 5;
|
||||
optional FLAG queryStatusV3Thumbnail = 6;
|
||||
optional FLAG liveLocations = 7;
|
||||
optional FLAG queryVname = 8;
|
||||
optional FLAG voipIndividualIncoming = 9;
|
||||
optional FLAG quickRepliesQuery = 10;
|
||||
}
|
||||
78
vendor/github.com/Rhymen/go-whatsapp/binary/token/token.go
generated
vendored
Normal file
78
vendor/github.com/Rhymen/go-whatsapp/binary/token/token.go
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
package token
|
||||
|
||||
import "fmt"
|
||||
|
||||
var SingleByteTokens = [...]string{"", "", "", "200", "400", "404", "500", "501", "502", "action", "add",
|
||||
"after", "archive", "author", "available", "battery", "before", "body",
|
||||
"broadcast", "chat", "clear", "code", "composing", "contacts", "count",
|
||||
"create", "debug", "delete", "demote", "duplicate", "encoding", "error",
|
||||
"false", "filehash", "from", "g.us", "group", "groups_v2", "height", "id",
|
||||
"image", "in", "index", "invis", "item", "jid", "kind", "last", "leave",
|
||||
"live", "log", "media", "message", "mimetype", "missing", "modify", "name",
|
||||
"notification", "notify", "out", "owner", "participant", "paused",
|
||||
"picture", "played", "presence", "preview", "promote", "query", "raw",
|
||||
"read", "receipt", "received", "recipient", "recording", "relay",
|
||||
"remove", "response", "resume", "retry", "s.whatsapp.net", "seconds",
|
||||
"set", "size", "status", "subject", "subscribe", "t", "text", "to", "true",
|
||||
"type", "unarchive", "unavailable", "url", "user", "value", "web", "width",
|
||||
"mute", "read_only", "admin", "creator", "short", "update", "powersave",
|
||||
"checksum", "epoch", "block", "previous", "409", "replaced", "reason",
|
||||
"spam", "modify_tag", "message_info", "delivery", "emoji", "title",
|
||||
"description", "canonical-url", "matched-text", "star", "unstar",
|
||||
"media_key", "filename", "identity", "unread", "page", "page_count",
|
||||
"search", "media_message", "security", "call_log", "profile", "ciphertext",
|
||||
"invite", "gif", "vcard", "frequent", "privacy", "blacklist", "whitelist",
|
||||
"verify", "location", "document", "elapsed", "revoke_invite", "expiration",
|
||||
"unsubscribe", "disable", "vname", "old_jid", "new_jid", "announcement",
|
||||
"locked", "prop", "label", "color", "call", "offer", "call-id"}
|
||||
|
||||
var doubleByteTokens = [...]string{}
|
||||
|
||||
func GetSingleToken(i int) (string, error) {
|
||||
if i < 3 || i >= len(SingleByteTokens) {
|
||||
return "", fmt.Errorf("index out of single byte token bounds %d", i)
|
||||
}
|
||||
|
||||
return SingleByteTokens[i], nil
|
||||
}
|
||||
|
||||
func GetDoubleToken(index1 int, index2 int) (string, error) {
|
||||
n := 256*index1 + index2
|
||||
if n < 0 || n >= len(doubleByteTokens) {
|
||||
return "", fmt.Errorf("index out of double byte token bounds %d", n)
|
||||
}
|
||||
|
||||
return doubleByteTokens[n], nil
|
||||
}
|
||||
|
||||
func IndexOfSingleToken(token string) int {
|
||||
for i, t := range SingleByteTokens {
|
||||
if t == token {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
const (
|
||||
LIST_EMPTY = 0
|
||||
STREAM_END = 2
|
||||
DICTIONARY_0 = 236
|
||||
DICTIONARY_1 = 237
|
||||
DICTIONARY_2 = 238
|
||||
DICTIONARY_3 = 239
|
||||
LIST_8 = 248
|
||||
LIST_16 = 249
|
||||
JID_PAIR = 250
|
||||
HEX_8 = 251
|
||||
BINARY_8 = 252
|
||||
BINARY_20 = 253
|
||||
BINARY_32 = 254
|
||||
NIBBLE_8 = 255
|
||||
)
|
||||
|
||||
const (
|
||||
PACKED_MAX = 254
|
||||
SINGLE_BYTE_MAX = 256
|
||||
)
|
||||
210
vendor/github.com/Rhymen/go-whatsapp/conn.go
generated
vendored
Normal file
210
vendor/github.com/Rhymen/go-whatsapp/conn.go
generated
vendored
Normal file
@@ -0,0 +1,210 @@
|
||||
//Package whatsapp provides a developer API to interact with the WhatsAppWeb-Servers.
|
||||
package whatsapp
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type metric byte
|
||||
|
||||
const (
|
||||
debugLog metric = iota + 1
|
||||
queryResume
|
||||
queryReceipt
|
||||
queryMedia
|
||||
queryChat
|
||||
queryContacts
|
||||
queryMessages
|
||||
presence
|
||||
presenceSubscribe
|
||||
group
|
||||
read
|
||||
chat
|
||||
received
|
||||
pic
|
||||
status
|
||||
message
|
||||
queryActions
|
||||
block
|
||||
queryGroup
|
||||
queryPreview
|
||||
queryEmoji
|
||||
queryMessageInfo
|
||||
spam
|
||||
querySearch
|
||||
queryIdentity
|
||||
queryUrl
|
||||
profile
|
||||
contact
|
||||
queryVcard
|
||||
queryStatus
|
||||
queryStatusUpdate
|
||||
privacyStatus
|
||||
queryLiveLocations
|
||||
liveLocation
|
||||
queryVname
|
||||
queryLabels
|
||||
call
|
||||
queryCall
|
||||
queryQuickReplies
|
||||
)
|
||||
|
||||
type flag byte
|
||||
|
||||
const (
|
||||
ignore flag = 1 << (7 - iota)
|
||||
ackRequest
|
||||
available
|
||||
notAvailable
|
||||
expires
|
||||
skipOffline
|
||||
)
|
||||
|
||||
/*
|
||||
Conn is created by NewConn. Interacting with the initialized Conn is the main way of interacting with our package.
|
||||
It holds all necessary information to make the package work internally.
|
||||
*/
|
||||
type Conn struct {
|
||||
ws *websocketWrapper
|
||||
listener *listenerWrapper
|
||||
|
||||
connected bool
|
||||
loggedIn bool
|
||||
wg *sync.WaitGroup
|
||||
|
||||
session *Session
|
||||
sessionLock uint32
|
||||
handler []Handler
|
||||
msgCount int
|
||||
msgTimeout time.Duration
|
||||
Info *Info
|
||||
Store *Store
|
||||
ServerLastSeen time.Time
|
||||
|
||||
longClientName string
|
||||
shortClientName string
|
||||
}
|
||||
|
||||
type websocketWrapper struct {
|
||||
sync.Mutex
|
||||
conn *websocket.Conn
|
||||
close chan struct{}
|
||||
}
|
||||
|
||||
type listenerWrapper struct {
|
||||
sync.RWMutex
|
||||
m map[string]chan string
|
||||
}
|
||||
|
||||
/*
|
||||
Creates a new connection with a given timeout. The websocket connection to the WhatsAppWeb servers get´s established.
|
||||
The goroutine for handling incoming messages is started
|
||||
*/
|
||||
func NewConn(timeout time.Duration) (*Conn, error) {
|
||||
wac := &Conn{
|
||||
handler: make([]Handler, 0),
|
||||
msgCount: 0,
|
||||
msgTimeout: timeout,
|
||||
Store: newStore(),
|
||||
|
||||
longClientName: "github.com/rhymen/go-whatsapp",
|
||||
shortClientName: "go-whatsapp",
|
||||
}
|
||||
return wac, wac.connect()
|
||||
}
|
||||
|
||||
// connect should be guarded with wsWriteMutex
|
||||
func (wac *Conn) connect() (err error) {
|
||||
if wac.connected {
|
||||
return ErrAlreadyConnected
|
||||
}
|
||||
wac.connected = true
|
||||
defer func() { // set connected to false on error
|
||||
if err != nil {
|
||||
wac.connected = false
|
||||
}
|
||||
}()
|
||||
|
||||
dialer := &websocket.Dialer{
|
||||
ReadBufferSize: 25 * 1024 * 1024,
|
||||
WriteBufferSize: 10 * 1024 * 1024,
|
||||
HandshakeTimeout: wac.msgTimeout,
|
||||
}
|
||||
|
||||
headers := http.Header{"Origin": []string{"https://web.whatsapp.com"}}
|
||||
wsConn, _, err := dialer.Dial("wss://web.whatsapp.com/ws", headers)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "couldn't dial whatsapp web websocket")
|
||||
}
|
||||
|
||||
wsConn.SetCloseHandler(func(code int, text string) error {
|
||||
// from default CloseHandler
|
||||
message := websocket.FormatCloseMessage(code, "")
|
||||
err := wsConn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
|
||||
|
||||
// our close handling
|
||||
_, _ = wac.Disconnect()
|
||||
wac.handle(&ErrConnectionClosed{Code: code, Text: text})
|
||||
return err
|
||||
})
|
||||
|
||||
wac.ws = &websocketWrapper{
|
||||
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
|
||||
}
|
||||
|
||||
func (wac *Conn) Disconnect() (Session, error) {
|
||||
if !wac.connected {
|
||||
return Session{}, ErrNotConnected
|
||||
}
|
||||
wac.connected = false
|
||||
wac.loggedIn = false
|
||||
|
||||
close(wac.ws.close) //signal close
|
||||
wac.wg.Wait() //wait for close
|
||||
|
||||
err := wac.ws.conn.Close()
|
||||
wac.ws = nil
|
||||
|
||||
if wac.session == nil {
|
||||
return Session{}, err
|
||||
}
|
||||
return *wac.session, err
|
||||
}
|
||||
|
||||
func (wac *Conn) keepAlive(minIntervalMs int, maxIntervalMs int) {
|
||||
defer wac.wg.Done()
|
||||
|
||||
for {
|
||||
err := wac.sendKeepAlive()
|
||||
if err != nil {
|
||||
wac.handle(errors.Wrap(err, "keepAlive failed"))
|
||||
//TODO: Consequences?
|
||||
}
|
||||
interval := rand.Intn(maxIntervalMs-minIntervalMs) + minIntervalMs
|
||||
select {
|
||||
case <-time.After(time.Duration(interval) * time.Millisecond):
|
||||
case <-wac.ws.close:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
234
vendor/github.com/Rhymen/go-whatsapp/contact.go
generated
vendored
Normal file
234
vendor/github.com/Rhymen/go-whatsapp/contact.go
generated
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
package whatsapp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Rhymen/go-whatsapp/binary"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Presence string
|
||||
|
||||
const (
|
||||
PresenceAvailable = "available"
|
||||
PresenceUnavailable = "unavailable"
|
||||
PresenceComposing = "composing"
|
||||
PresenceRecording = "recording"
|
||||
PresencePaused = "paused"
|
||||
)
|
||||
|
||||
//TODO: filename? WhatsApp uses Store.Contacts for these functions
|
||||
// functions probably shouldn't return a string, maybe build a struct / return json
|
||||
// check for further queries
|
||||
func (wac *Conn) GetProfilePicThumb(jid string) (<-chan string, error) {
|
||||
data := []interface{}{"query", "ProfilePicThumb", jid}
|
||||
return wac.writeJson(data)
|
||||
}
|
||||
|
||||
func (wac *Conn) GetStatus(jid string) (<-chan string, error) {
|
||||
data := []interface{}{"query", "Status", jid}
|
||||
return wac.writeJson(data)
|
||||
}
|
||||
|
||||
func (wac *Conn) SubscribePresence(jid string) (<-chan string, error) {
|
||||
data := []interface{}{"action", "presence", "subscribe", jid}
|
||||
return wac.writeJson(data)
|
||||
}
|
||||
|
||||
func (wac *Conn) Search(search string, count, page int) (*binary.Node, error) {
|
||||
return wac.query("search", "", "", "", "", search, count, page)
|
||||
}
|
||||
|
||||
func (wac *Conn) LoadMessages(jid, messageId string, count int) (*binary.Node, error) {
|
||||
return wac.query("message", jid, "", "before", "true", "", count, 0)
|
||||
}
|
||||
|
||||
func (wac *Conn) LoadMessagesBefore(jid, messageId string, count int) (*binary.Node, error) {
|
||||
return wac.query("message", jid, messageId, "before", "true", "", count, 0)
|
||||
}
|
||||
|
||||
func (wac *Conn) LoadMessagesAfter(jid, messageId string, count int) (*binary.Node, error) {
|
||||
return wac.query("message", jid, messageId, "after", "true", "", count, 0)
|
||||
}
|
||||
|
||||
func (wac *Conn) Presence(jid string, presence Presence) (<-chan string, error) {
|
||||
ts := time.Now().Unix()
|
||||
tag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
|
||||
|
||||
content := binary.Node{
|
||||
Description: "presence",
|
||||
Attributes: map[string]string{
|
||||
"type": string(presence),
|
||||
},
|
||||
}
|
||||
switch presence {
|
||||
case PresenceComposing:
|
||||
fallthrough
|
||||
case PresenceRecording:
|
||||
fallthrough
|
||||
case PresencePaused:
|
||||
content.Attributes["to"] = jid
|
||||
}
|
||||
|
||||
n := binary.Node{
|
||||
Description: "action",
|
||||
Attributes: map[string]string{
|
||||
"type": "set",
|
||||
"epoch": strconv.Itoa(wac.msgCount),
|
||||
},
|
||||
Content: []interface{}{content},
|
||||
}
|
||||
|
||||
return wac.writeBinary(n, group, ignore, tag)
|
||||
}
|
||||
|
||||
func (wac *Conn) Exist(jid string) (<-chan string, error) {
|
||||
data := []interface{}{"query", "exist", jid}
|
||||
return wac.writeJson(data)
|
||||
}
|
||||
|
||||
func (wac *Conn) Emoji() (*binary.Node, error) {
|
||||
return wac.query("emoji", "", "", "", "", "", 0, 0)
|
||||
}
|
||||
|
||||
func (wac *Conn) Contacts() (*binary.Node, error) {
|
||||
return wac.query("contacts", "", "", "", "", "", 0, 0)
|
||||
}
|
||||
|
||||
func (wac *Conn) Chats() (*binary.Node, error) {
|
||||
return wac.query("chat", "", "", "", "", "", 0, 0)
|
||||
}
|
||||
|
||||
func (wac *Conn) Read(jid, id string) (<-chan string, error) {
|
||||
ts := time.Now().Unix()
|
||||
tag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
|
||||
|
||||
n := binary.Node{
|
||||
Description: "action",
|
||||
Attributes: map[string]string{
|
||||
"type": "set",
|
||||
"epoch": strconv.Itoa(wac.msgCount),
|
||||
},
|
||||
Content: []interface{}{binary.Node{
|
||||
Description: "read",
|
||||
Attributes: map[string]string{
|
||||
"count": "1",
|
||||
"index": id,
|
||||
"jid": jid,
|
||||
"owner": "false",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
return wac.writeBinary(n, group, ignore, tag)
|
||||
}
|
||||
|
||||
func (wac *Conn) query(t, jid, messageId, kind, owner, search string, count, page int) (*binary.Node, error) {
|
||||
ts := time.Now().Unix()
|
||||
tag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
|
||||
|
||||
n := binary.Node{
|
||||
Description: "query",
|
||||
Attributes: map[string]string{
|
||||
"type": t,
|
||||
"epoch": strconv.Itoa(wac.msgCount),
|
||||
},
|
||||
}
|
||||
|
||||
if jid != "" {
|
||||
n.Attributes["jid"] = jid
|
||||
}
|
||||
|
||||
if messageId != "" {
|
||||
n.Attributes["index"] = messageId
|
||||
}
|
||||
|
||||
if kind != "" {
|
||||
n.Attributes["kind"] = kind
|
||||
}
|
||||
|
||||
if owner != "" {
|
||||
n.Attributes["owner"] = owner
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
n.Attributes["search"] = search
|
||||
}
|
||||
|
||||
if count != 0 {
|
||||
n.Attributes["count"] = strconv.Itoa(count)
|
||||
}
|
||||
|
||||
if page != 0 {
|
||||
n.Attributes["page"] = strconv.Itoa(page)
|
||||
}
|
||||
|
||||
ch, err := wac.writeBinary(n, group, ignore, tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg, err := wac.decryptBinaryMessage([]byte(<-ch))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//TODO: use parseProtoMessage
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (wac *Conn) setGroup(t, jid, subject string, participants []string) (<-chan string, error) {
|
||||
ts := time.Now().Unix()
|
||||
tag := fmt.Sprintf("%d.--%d", ts, wac.msgCount)
|
||||
|
||||
//TODO: get proto or improve encoder to handle []interface{}
|
||||
|
||||
p := buildParticipantNodes(participants)
|
||||
|
||||
g := binary.Node{
|
||||
Description: "group",
|
||||
Attributes: map[string]string{
|
||||
"author": wac.session.Wid,
|
||||
"id": tag,
|
||||
"type": t,
|
||||
},
|
||||
Content: p,
|
||||
}
|
||||
|
||||
if jid != "" {
|
||||
g.Attributes["jid"] = jid
|
||||
}
|
||||
|
||||
if subject != "" {
|
||||
g.Attributes["subject"] = subject
|
||||
}
|
||||
|
||||
n := binary.Node{
|
||||
Description: "action",
|
||||
Attributes: map[string]string{
|
||||
"type": "set",
|
||||
"epoch": strconv.Itoa(wac.msgCount),
|
||||
},
|
||||
Content: []interface{}{g},
|
||||
}
|
||||
|
||||
return wac.writeBinary(n, group, ignore, tag)
|
||||
}
|
||||
|
||||
func buildParticipantNodes(participants []string) []binary.Node {
|
||||
l := len(participants)
|
||||
if participants == nil || l == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
p := make([]binary.Node, len(participants))
|
||||
for i, participant := range participants {
|
||||
p[i] = binary.Node{
|
||||
Description: "participant",
|
||||
Attributes: map[string]string{
|
||||
"jid": participant,
|
||||
},
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
101
vendor/github.com/Rhymen/go-whatsapp/crypto/cbc/cbc.go
generated
vendored
Normal file
101
vendor/github.com/Rhymen/go-whatsapp/crypto/cbc/cbc.go
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
CBC describes a block cipher mode. In cryptography, a block cipher mode of operation is an algorithm that uses a
|
||||
block cipher to provide an information service such as confidentiality or authenticity. A block cipher by itself
|
||||
is only suitable for the secure cryptographic transformation (encryption or decryption) of one fixed-length group of
|
||||
bits called a block. A mode of operation describes how to repeatedly apply a cipher's single-block operation to
|
||||
securely transform amounts of data larger than a block.
|
||||
|
||||
This package simplifies the usage of AES-256-CBC.
|
||||
*/
|
||||
package cbc
|
||||
|
||||
/*
|
||||
Some code is provided by the GitHub user locked (github.com/locked):
|
||||
https://gist.github.com/locked/b066aa1ddeb2b28e855e
|
||||
Thanks!
|
||||
*/
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
/*
|
||||
Decrypt is a function that decrypts a given cipher text with a provided key and initialization vector(iv).
|
||||
*/
|
||||
func Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return nil, fmt.Errorf("ciphertext is shorter then block size: %d / %d", len(ciphertext), aes.BlockSize)
|
||||
}
|
||||
|
||||
if iv == nil {
|
||||
iv = ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
}
|
||||
|
||||
cbc := cipher.NewCBCDecrypter(block, iv)
|
||||
cbc.CryptBlocks(ciphertext, ciphertext)
|
||||
|
||||
return unpad(ciphertext)
|
||||
}
|
||||
|
||||
/*
|
||||
Encrypt is a function that encrypts plaintext with a given key and an optional initialization vector(iv).
|
||||
*/
|
||||
func Encrypt(key, iv, plaintext []byte) ([]byte, error) {
|
||||
plaintext = pad(plaintext, aes.BlockSize)
|
||||
|
||||
if len(plaintext)%aes.BlockSize != 0 {
|
||||
return nil, fmt.Errorf("plaintext is not a multiple of the block size: %d / %d", len(plaintext), aes.BlockSize)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ciphertext []byte
|
||||
if iv == nil {
|
||||
ciphertext = make([]byte, aes.BlockSize+len(plaintext))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cbc := cipher.NewCBCEncrypter(block, iv)
|
||||
cbc.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
|
||||
} else {
|
||||
ciphertext = make([]byte, len(plaintext))
|
||||
|
||||
cbc := cipher.NewCBCEncrypter(block, iv)
|
||||
cbc.CryptBlocks(ciphertext, plaintext)
|
||||
}
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func pad(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
func unpad(src []byte) ([]byte, error) {
|
||||
length := len(src)
|
||||
padLen := int(src[length-1])
|
||||
|
||||
if padLen > length {
|
||||
return nil, fmt.Errorf("padding is greater then the length: %d / %d", padLen, length)
|
||||
}
|
||||
|
||||
return src[:(length - padLen)], nil
|
||||
}
|
||||
44
vendor/github.com/Rhymen/go-whatsapp/crypto/curve25519/curve.go
generated
vendored
Normal file
44
vendor/github.com/Rhymen/go-whatsapp/crypto/curve25519/curve.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
In cryptography, Curve25519 is an elliptic curve offering 128 bits of security and designed for use with the elliptic
|
||||
curve Diffie–Hellman (ECDH) key agreement scheme. It is one of the fastest ECC curves and is not covered by any known
|
||||
patents. The reference implementation is public domain software. The original Curve25519 paper defined it
|
||||
as a Diffie–Hellman (DH) function.
|
||||
*/
|
||||
package curve25519
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"io"
|
||||
)
|
||||
|
||||
/*
|
||||
GenerateKey generates a public private key pair using Curve25519.
|
||||
*/
|
||||
func GenerateKey() (privateKey *[32]byte, publicKey *[32]byte, err error) {
|
||||
var pub, priv [32]byte
|
||||
|
||||
_, err = io.ReadFull(rand.Reader, priv[:])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
priv[0] &= 248
|
||||
priv[31] &= 127
|
||||
priv[31] |= 64
|
||||
|
||||
curve25519.ScalarBaseMult(&pub, &priv)
|
||||
|
||||
return &priv, &pub, nil
|
||||
}
|
||||
|
||||
/*
|
||||
GenerateSharedSecret generates the shared secret with a given public private key pair.
|
||||
*/
|
||||
func GenerateSharedSecret(priv, pub [32]byte) []byte {
|
||||
var secret [32]byte
|
||||
|
||||
curve25519.ScalarMult(&secret, &priv, &pub)
|
||||
|
||||
return secret[:]
|
||||
}
|
||||
47
vendor/github.com/Rhymen/go-whatsapp/crypto/hkdf/hkdf.go
generated
vendored
Normal file
47
vendor/github.com/Rhymen/go-whatsapp/crypto/hkdf/hkdf.go
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
HKDF is a simple key derivation function (KDF) based on
|
||||
a hash-based message authentication code (HMAC). It was initially proposed by its authors as a building block in
|
||||
various protocols and applications, as well as to discourage the proliferation of multiple KDF mechanisms.
|
||||
The main approach HKDF follows is the "extract-then-expand" paradigm, where the KDF logically consists of two modules:
|
||||
the first stage takes the input keying material and "extracts" from it a fixed-length pseudorandom key, and then the
|
||||
second stage "expands" this key into several additional pseudorandom keys (the output of the KDF).
|
||||
*/
|
||||
package hkdf
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
"io"
|
||||
)
|
||||
|
||||
/*
|
||||
Expand expands a given key with the HKDF algorithm.
|
||||
*/
|
||||
func Expand(key []byte, length int, info string) ([]byte, error) {
|
||||
var h io.Reader
|
||||
if info == "" {
|
||||
/*
|
||||
Only used during initial login
|
||||
Pseudorandom Key is provided by server and has not to be created
|
||||
*/
|
||||
h = hkdf.Expand(sha256.New, key, []byte(info))
|
||||
} else {
|
||||
/*
|
||||
Used every other time
|
||||
Pseudorandom Key is created during kdf.New
|
||||
This is the normal that crypto/hkdf is used
|
||||
*/
|
||||
h = hkdf.New(sha256.New, key, nil, []byte(info))
|
||||
}
|
||||
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
35
vendor/github.com/Rhymen/go-whatsapp/errors.go
generated
vendored
Normal 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)
|
||||
}
|
||||
12
vendor/github.com/Rhymen/go-whatsapp/go.mod
generated
vendored
Normal file
12
vendor/github.com/Rhymen/go-whatsapp/go.mod
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
module github.com/Rhymen/go-whatsapp
|
||||
|
||||
require (
|
||||
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/pkg/errors v0.8.1
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
|
||||
)
|
||||
35
vendor/github.com/Rhymen/go-whatsapp/go.sum
generated
vendored
Normal file
35
vendor/github.com/Rhymen/go-whatsapp/go.sum
generated
vendored
Normal file
@@ -0,0 +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.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/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-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/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
90
vendor/github.com/Rhymen/go-whatsapp/group.go
generated
vendored
Normal 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
|
||||
}
|
||||
268
vendor/github.com/Rhymen/go-whatsapp/handler.go
generated
vendored
Normal file
268
vendor/github.com/Rhymen/go-whatsapp/handler.go
generated
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
package whatsapp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp/binary"
|
||||
"github.com/Rhymen/go-whatsapp/binary/proto"
|
||||
)
|
||||
|
||||
/*
|
||||
The Handler interface is the minimal interface that needs to be implemented
|
||||
to be accepted as a valid handler for our dispatching system.
|
||||
The minimal handler is used to dispatch error messages. These errors occur on unexpected behavior by the websocket
|
||||
connection or if we are unable to handle or interpret an incoming message. Error produced by user actions are not
|
||||
dispatched through this handler. They are returned as an error on the specific function call.
|
||||
*/
|
||||
type Handler interface {
|
||||
HandleError(err error)
|
||||
}
|
||||
|
||||
/*
|
||||
The TextMessageHandler interface needs to be implemented to receive text messages dispatched by the dispatcher.
|
||||
*/
|
||||
type TextMessageHandler interface {
|
||||
Handler
|
||||
HandleTextMessage(message TextMessage)
|
||||
}
|
||||
|
||||
/*
|
||||
The ImageMessageHandler interface needs to be implemented to receive image messages dispatched by the dispatcher.
|
||||
*/
|
||||
type ImageMessageHandler interface {
|
||||
Handler
|
||||
HandleImageMessage(message ImageMessage)
|
||||
}
|
||||
|
||||
/*
|
||||
The VideoMessageHandler interface needs to be implemented to receive video messages dispatched by the dispatcher.
|
||||
*/
|
||||
type VideoMessageHandler interface {
|
||||
Handler
|
||||
HandleVideoMessage(message VideoMessage)
|
||||
}
|
||||
|
||||
/*
|
||||
The AudioMessageHandler interface needs to be implemented to receive audio messages dispatched by the dispatcher.
|
||||
*/
|
||||
type AudioMessageHandler interface {
|
||||
Handler
|
||||
HandleAudioMessage(message AudioMessage)
|
||||
}
|
||||
|
||||
/*
|
||||
The DocumentMessageHandler interface needs to be implemented to receive document messages dispatched by the dispatcher.
|
||||
*/
|
||||
type DocumentMessageHandler interface {
|
||||
Handler
|
||||
HandleDocumentMessage(message DocumentMessage)
|
||||
}
|
||||
|
||||
/*
|
||||
The JsonMessageHandler interface needs to be implemented to receive json messages dispatched by the dispatcher.
|
||||
These json messages contain status updates of every kind sent by WhatsAppWeb servers. WhatsAppWeb uses these messages
|
||||
to built a Store, which is used to save these "secondary" information. These messages may contain
|
||||
presence (available, last see) information, or just the battery status of your phone.
|
||||
*/
|
||||
type JsonMessageHandler interface {
|
||||
Handler
|
||||
HandleJsonMessage(message string)
|
||||
}
|
||||
|
||||
/**
|
||||
The RawMessageHandler interface needs to be implemented to receive raw messages dispatched by the dispatcher.
|
||||
Raw messages are the raw protobuf structs instead of the easy-to-use structs in TextMessageHandler, ImageMessageHandler, etc..
|
||||
*/
|
||||
type RawMessageHandler interface {
|
||||
Handler
|
||||
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.
|
||||
The provided handler must at least implement the Handler interface. Additionally implemented
|
||||
handlers(TextMessageHandler, ImageMessageHandler) are optional. At runtime it is checked if they are implemented
|
||||
and they are called if so and needed.
|
||||
*/
|
||||
func (wac *Conn) AddHandler(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{}) {
|
||||
switch m := message.(type) {
|
||||
case error:
|
||||
for _, h := range wac.handler {
|
||||
go h.HandleError(m)
|
||||
}
|
||||
case string:
|
||||
for _, h := range wac.handler {
|
||||
if x, ok := h.(JsonMessageHandler); ok {
|
||||
go x.HandleJsonMessage(m)
|
||||
}
|
||||
}
|
||||
case TextMessage:
|
||||
for _, h := range wac.handler {
|
||||
if x, ok := h.(TextMessageHandler); ok {
|
||||
go x.HandleTextMessage(m)
|
||||
}
|
||||
}
|
||||
case ImageMessage:
|
||||
for _, h := range wac.handler {
|
||||
if x, ok := h.(ImageMessageHandler); ok {
|
||||
go x.HandleImageMessage(m)
|
||||
}
|
||||
}
|
||||
case VideoMessage:
|
||||
for _, h := range wac.handler {
|
||||
if x, ok := h.(VideoMessageHandler); ok {
|
||||
go x.HandleVideoMessage(m)
|
||||
}
|
||||
}
|
||||
case AudioMessage:
|
||||
for _, h := range wac.handler {
|
||||
if x, ok := h.(AudioMessageHandler); ok {
|
||||
go x.HandleAudioMessage(m)
|
||||
}
|
||||
}
|
||||
case DocumentMessage:
|
||||
for _, h := range wac.handler {
|
||||
if x, ok := h.(DocumentMessageHandler); ok {
|
||||
go x.HandleDocumentMessage(m)
|
||||
}
|
||||
}
|
||||
case *proto.WebMessageInfo:
|
||||
for _, h := range wac.handler {
|
||||
if x, ok := h.(RawMessageHandler); ok {
|
||||
go x.HandleRawMessage(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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{}) {
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch message := msg.(type) {
|
||||
case *binary.Node:
|
||||
if message.Description == "action" {
|
||||
if con, ok := message.Content.([]interface{}); ok {
|
||||
for a := range con {
|
||||
if v, ok := con[a].(*proto.WebMessageInfo); ok {
|
||||
wac.handle(v)
|
||||
wac.handle(parseProtoMessage(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if message.Description == "response" && message.Attributes["type"] == "contacts" {
|
||||
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:
|
||||
wac.handle(message)
|
||||
case string:
|
||||
wac.handle(message)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown type in dipatcher chan: %T", msg)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user