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

Compare commits

..

2 Commits

Author SHA1 Message Date
Wim
612acfddff Release v1.6.1 2017-12-26 19:20:44 +01:00
Wim
932b80d4f7 Fix regression. Closes #323 2017-12-26 19:14:36 +01:00
2070 changed files with 323805 additions and 283721 deletions

View File

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

View File

@ -1,26 +0,0 @@
---
name: Bug report
about: Create a report to help us improve. (Check the FAQ on the wiki first)
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots/debug logs**
If applicable, add screenshots to help explain your problem.
Use logs from running `matterbridge -debug` if possible.
**Environment (please complete the following information):**
- OS: [e.g. linux]
- Matterbridge version: output of `matterbridge -version`
- If self compiled: output of `git rev-parse HEAD`
**Additional context**
Please add your configuration file (be sure to exclude or anonymize private data (tokens/passwords))

View File

@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

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

View File

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

View File

@ -1,18 +1,16 @@
language: go language: go
go: go:
- 1.11.x #- 1.7.x
go_import_path: github.com/42wim/matterbridge - 1.9.x
# - tip
# we have everything vendored # we have everything vendored
install: true install: true
git:
depth: 200
env: env:
global:
- GOOS=linux GOARCH=amd64 - GOOS=linux GOARCH=amd64
- GOLANGCI_VERSION="v1.14.0" # - GOOS=windows GOARCH=amd64
#- GOOS=linux GOARCH=arm
matrix: matrix:
# It's ok if our code fails on unstable development versions of Go. # It's ok if our code fails on unstable development versions of Go.
@ -26,38 +24,26 @@ notifications:
email: false email: false
before_script: before_script:
# Get version info from tags. - MY_VERSION=$(git describe --tags)
- MY_VERSION="$(git describe --tags)" - GO_FILES=$(find . -iname '*.go' | grep -v /vendor/) # All the .go files, excluding vendor/
# Retrieve the golangci-lint linter binary. - PKGS=$(go list ./... | grep -v /vendor/) # All the import paths, excluding vendor/
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION} # - go get github.com/golang/lint/golint # Linter
# Retrieve and prepare CodeClimate's test coverage reporter. - go get honnef.co/go/tools/cmd/megacheck # Badass static analyzer/linter
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
# Anything in before_script: that returns a nonzero exit code will
# flunk the build and immediately stop. It's sorta like having
# set -e enabled in bash.
script: script:
# Run the linter. - test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt
- golangci-lint run - go test -v -race $PKGS # Run all the tests with the race detector enabled
# Run all the tests with the race detector and generate coverage. - go vet $PKGS # go vet is the official Go static analyzer
- go test -v -race -coverprofile c.out ./... - megacheck $PKGS # "go vet on steroids" + linter
# Run the build script to generate the necessary binaries and images.
- /bin/bash ci/bintray.sh - /bin/bash ci/bintray.sh
#- golint -set_exit_status $PKGS # one last linter
after_script:
# Upload test coverage to CodeClimate.
- ./cc-test-reporter after-build --exit-code ${TRAVIS_TEST_RESULT}
deploy: deploy:
on:
all_branches: true
provider: bintray provider: bintray
on:
all_branches: true
edge:
branch: v1.8.47
file: ci/deploy.json file: ci/deploy.json
user: 42wim user: 42wim
on:
all_branches: true
key: key:
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI=" secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="

206
README.md
View File

@ -1,76 +1,45 @@
<div align="center">
# matterbridge # matterbridge
Click on one of the badges below to join the chat
![Matterbridge Logo](img/matterbridge-notext.gif)<br /> [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?colorB=42f4242)](https://gitter.im/42wim/matterbridge) [![Join the IRC chat at https://webchat.freenode.net/?channels=matterbridgechat](https://img.shields.io/badge/IRC-matterbridgechat-green.svg?colorB=42f4242)](https://webchat.freenode.net/?channels=matterbridgechat) [![Discord](https://img.shields.io/badge/discord-matterbridge-green.svg?colorB=42f4242)](https://discord.gg/AkKPtrQ) [![Matrix](https://img.shields.io/badge/matrix-matterbridge-green.svg?colorB=42f4242)](https://riot.im/app/#/room/#matterbridge:matrix.org) [![Slack](https://img.shields.io/badge/slack-matterbridgechat-green.svg?colorB=42f4242)](https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA) [![Mattermost](https://img.shields.io/badge/mattermost-matterbridge-green.svg?colorB=42f4242)](https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e) ![Xmpp](https://img.shields.io/badge/xmpp-matterbridge@muc.im.koderoot.net-green.svg?colorB=42f4242)
**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 or join the development chat.</sub>
<sup> [![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest) [![Download dev](https://img.shields.io/bintray/v/42wim/nightly/Matterbridge.svg?label=download%20dev&colorB=007ec6)](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
[Gitter][mb-gitter] | ![matterbridge.gif](https://s15.postimg.org/qpjhp6y3f/matterbridge.gif)
[IRC][mb-irc] |
[Discord][mb-discord] |
[Matrix][mb-matrix] |
[Slack][mb-slack] |
[Mattermost][mb-mattermost] |
[Rocket.Chat][mb-rocketchat] |
[XMPP][mb-xmpp] |
[Twitch][mb-twitch] |
[WhatsApp][mb-whatsapp] |
[Zulip][mb-zulip] |
[Telegram][mb-telegram] |
And more...
</sup>
---- Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix and Steam.
[![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest) Has a REST API.
[![Download dev](https://img.shields.io/bintray/v/42wim/nightly/Matterbridge.svg?label=download%20dev&colorB=007ec6)](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
[![Maintainability](https://api.codeclimate.com/v1/badges/82dff70ef2ba85a6173a/maintainability)](https://codeclimate.com/github/42wim/matterbridge/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/82dff70ef2ba85a6173a/test_coverage)](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br />
<hr />
</div>
<div align="right"><sup>
**Note:** Matter<em>most</em> isn't required to run matter<em>bridge</em>.</sup></div> # Table of Contents
* [Features](#features)
### Table of Contents * [Requirements](#requirements)
* [Features](https://github.com/42wim/matterbridge/wiki/Features)
* [Natively supported](#natively-supported)
* [3rd party via matterbridge api](#3rd-party-via-matterbridge-api)
* [API](#API)
* [Chat with us](#chat-with-us)
* [Screenshots](https://github.com/42wim/matterbridge/wiki/) * [Screenshots](https://github.com/42wim/matterbridge/wiki/)
* [Installing](#installing) * [Installing](#installing)
* [Binaries](#binaries) * [Binaries](#binaries)
* [Building](#building) * [Building](#building)
* [Configuration](#configuration) * [Configuration](#configuration)
* [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) * [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config)
* [Examples](#examples) * [Examples](#examples)
* [Running](#running) * [Running](#running)
* [Docker](#docker) * [Docker](#docker)
* [Changelog](#changelog) * [Changelog](#changelog)
* [FAQ](#faq) * [FAQ](#faq)
* [Related projects](#related-projects)
* [Articles](#articles)
* [Thanks](#thanks) * [Thanks](#thanks)
## Features # Features
* [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols) * Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat (via xmpp), Matrix and Steam.
* [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols) Pick and mix.
* [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes) * Support private groups on your mattermost/slack.
* Preserves threading when possible * Allow for bridging the same bridges, which means you can eg bridge between multiple mattermosts.
* [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling) * The bridge is now a gateway which has support multiple in and out bridges. (and supports multiple gateways).
* [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing) * Edits and delete messages across bridges that support it (mattermost,slack,discord,gitter,telegram)
* [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups) * REST API to read/post messages to bridges (WIP).
* [API](https://github.com/42wim/matterbridge/wiki/Features#api)
### Natively supported # Requirements
Accounts to one of the supported bridges
* [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x * [Mattermost](https://github.com/mattermost/platform/) 3.8.x - 3.10.x, 4.x
* [IRC](http://www.mirc.com/servers.html) * [IRC](http://www.mirc.com/servers.html)
* [XMPP](https://xmpp.org) * [XMPP](https://jabber.org)
* [Gitter](https://gitter.im) * [Gitter](https://gitter.im)
* [Slack](https://slack.com) * [Slack](https://slack.com)
* [Discord](https://discordapp.com) * [Discord](https://discordapp.com)
@ -79,58 +48,17 @@
* [Rocket.chat](https://rocket.chat) * [Rocket.chat](https://rocket.chat)
* [Matrix](https://matrix.org) * [Matrix](https://matrix.org)
* [Steam](https://store.steampowered.com/) * [Steam](https://store.steampowered.com/)
* [Twitch](https://twitch.tv)
* [Ssh-chat](https://github.com/shazow/ssh-chat)
* [WhatsApp](https://www.whatsapp.com/)
* [Zulip](https://zulipchat.com)
### 3rd party via matterbridge api # Screenshots
* [Minecraft](https://github.com/elytra/MatterLink)
* [Reddit](https://github.com/bonehurtingjuice/mattereddit)
* [Facebook messenger](https://github.com/VictorNine/fbridge)
### API
The API is very basic at the moment.
More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api).
Used by the projects below. Feel free to make a PR to add your project to this list.
* [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
* [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
## Chat with us
Questions or want to test on your favorite platform? Join below:
* [Gitter][mb-gitter]
* [IRC][mb-irc]
* [Discord][mb-discord]
* [Matrix][mb-matrix]
* [Slack][mb-slack]
* [Mattermost][mb-mattermost]
* [Rocket.Chat][mb-rocketchat]
* [XMPP][mb-xmpp]
* [Twitch][mb-twitch]
* [Zulip][mb-zulip]
* [Telegram][mb-telegram]
## Screenshots
See https://github.com/42wim/matterbridge/wiki See https://github.com/42wim/matterbridge/wiki
## Installing # Installing
### Binaries ## Binaries
* Latest stable release [v1.13.1](https://github.com/42wim/matterbridge/releases/latest) * Latest stable release [v1.6.1](https://github.com/42wim/matterbridge/releases/latest)
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/) * Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
### Packages ## Building
* [Overview](https://repology.org/metapackage/matterbridge/versions) 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)
### Building
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.
``` ```
cd $GOPATH cd $GOPATH
@ -144,16 +72,16 @@ $ ls bin/
matterbridge matterbridge
``` ```
## Configuration # Configuration
### Basic configuration ## Basic configuration
See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
### Advanced configuration ## Advanced configuration
* [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example. * [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
### Examples ## Examples
#### Bridge mattermost (off-topic) - irc (#testing) ### Bridge mattermost (off-topic) - irc (#testing)
```toml ```
[irc] [irc]
[irc.freenode] [irc.freenode]
Server="irc.freenode.net:6667" Server="irc.freenode.net:6667"
@ -180,8 +108,8 @@ enable=true
channel="off-topic" channel="off-topic"
``` ```
#### Bridge slack (#general) - discord (general) ### Bridge slack (#general) - discord (general)
```toml ```
[slack] [slack]
[slack.test] [slack.test]
Token="yourslacktoken" Token="yourslacktoken"
@ -208,7 +136,7 @@ RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
channel = "general" channel = "general"
``` ```
## Running # Running
See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
@ -224,73 +152,33 @@ Usage of ./matterbridge:
show version show version
``` ```
### Docker ## Docker
Create your matterbridge.toml file locally eg in `/tmp/matterbridge.toml` Create your matterbridge.toml file locally eg in ```/tmp/matterbridge.toml```
``` ```
docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge
``` ```
## Changelog # Changelog
See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md) See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md)
## FAQ # FAQ
See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
## Related projects Want to tip ?
* [FOSSRIT/infrastructure - roles/matterbridge](https://github.com/FOSSRIT/infrastructure/tree/master/roles/matterbridge) (Ansible role used to automate deployments of Matterbridge) * eth: 0xb3f9b5387c66ad6be892bcb7bbc67862f3abc16f
* [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig) * btc: 1N7cKHj5SfqBHBzDJ6kad4BzeqUBBS2zhs
* [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
* [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
* [mattereddit](https://github.com/bonehurtingjuice/mattereddit)
* [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)
## 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/
* http://bencey.co.nz/2018/09/17/bridge/
* https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/
* https://kopano.com/blog/matterbridge-bridging-mattermost-chat/
* https://www.stitcher.com/s/?eid=52382713
* https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/
## Thanks
[![Digitalocean](https://snag.gy/3LVifX.jpg)](https://www.digitalocean.com/) for sponsoring demo/testing droplets.
# Thanks
Matterbridge wouldn't exist without these libraries: Matterbridge wouldn't exist without these libraries:
* discord - https://github.com/bwmarrin/discordgo * discord - https://github.com/bwmarrin/discordgo
* echo - https://github.com/labstack/echo * echo - https://github.com/labstack/echo
* gitter - https://github.com/sromku/go-gitter * gitter - https://github.com/sromku/go-gitter
* gops - https://github.com/google/gops * gops - https://github.com/google/gops
* gozulipbot - https://github.com/ifo/gozulipbot
* irc - https://github.com/lrstanley/girc * irc - https://github.com/lrstanley/girc
* mattermost - https://github.com/mattermost/mattermost-server * mattermost - https://github.com/mattermost/platform
* matrix - https://github.com/matrix-org/gomatrix * matrix - https://github.com/matrix-org/gomatrix
* sshchat - https://github.com/shazow/ssh-chat
* slack - https://github.com/nlopes/slack * slack - https://github.com/nlopes/slack
* steam - https://github.com/Philipp15b/go-steam * steam - https://github.com/Philipp15b/go-steam
* telegram - https://github.com/go-telegram-bot-api/telegram-bot-api * telegram - https://github.com/go-telegram-bot-api/telegram-bot-api
* xmpp - https://github.com/mattn/go-xmpp * 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 -->
[mb-gitter]: https://gitter.im/42wim/matterbridge
[mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat
[mb-discord]: https://discord.gg/AkKPtrQ
[mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org
[mb-slack]: https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA
[mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e
[mb-rocketchat]: https://open.rocket.chat/channel/matterbridge
[mb-xmpp]: https://inverse.chat/
[mb-twitch]: https://www.twitch.tv/matterbridge
[mb-whatsapp]: https://www.whatsapp.com/
[mb-zulip]: https://matterbridge.zulipchat.com/register/
[mb-telegram]: https://t.me/Matterbridge

View File

@ -2,24 +2,23 @@ package api
import ( import (
"encoding/json" "encoding/json"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"github.com/zfjagann/golang-ring"
"net/http" "net/http"
"sync" "sync"
"time" "time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/zfjagann/golang-ring"
) )
type API struct { type Api struct {
Messages ring.Ring Messages ring.Ring
sync.RWMutex sync.RWMutex
*bridge.Config *config.BridgeConfig
} }
type Message struct { type ApiMessage struct {
Text string `json:"text"` Text string `json:"text"`
Username string `json:"username"` Username string `json:"username"`
UserID string `json:"userid"` UserID string `json:"userid"`
@ -27,78 +26,75 @@ type Message struct {
Gateway string `json:"gateway"` Gateway string `json:"gateway"`
} }
func New(cfg *bridge.Config) bridge.Bridger { var flog *log.Entry
b := &API{Config: cfg} var protocol = "api"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Api {
b := &Api{BridgeConfig: cfg}
e := echo.New() e := echo.New()
e.HideBanner = true
e.HidePort = true
b.Messages = ring.Ring{} b.Messages = ring.Ring{}
if b.GetInt("Buffer") != 0 { b.Messages.SetCapacity(b.Config.Buffer)
b.Messages.SetCapacity(b.GetInt("Buffer")) if b.Config.Token != "" {
}
if b.GetString("Token") != "" {
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return key == b.GetString("Token"), nil return key == b.Config.Token, nil
})) }))
} }
e.GET("/api/health", b.handleHealthcheck)
e.GET("/api/messages", b.handleMessages) e.GET("/api/messages", b.handleMessages)
e.GET("/api/stream", b.handleStream) e.GET("/api/stream", b.handleStream)
e.POST("/api/message", b.handlePostMessage) e.POST("/api/message", b.handlePostMessage)
go func() { go func() {
if b.GetString("BindAddress") == "" { flog.Fatal(e.Start(b.Config.BindAddress))
b.Log.Fatalf("No BindAddress configured.")
}
b.Log.Infof("Listening on %s", b.GetString("BindAddress"))
b.Log.Fatal(e.Start(b.GetString("BindAddress")))
}() }()
return b return b
} }
func (b *API) Connect() error { func (b *Api) Connect() error {
return nil return nil
} }
func (b *API) Disconnect() error { func (b *Api) Disconnect() error {
return nil return nil
} }
func (b *API) JoinChannel(channel config.ChannelInfo) error { func (b *Api) JoinChannel(channel config.ChannelInfo) error {
return nil return nil
} }
func (b *API) Send(msg config.Message) (string, error) { func (b *Api) Send(msg config.Message) (string, error) {
b.Lock() b.Lock()
defer b.Unlock() defer b.Unlock()
// ignore delete messages // ignore delete messages
if msg.Event == config.EventMsgDelete { if msg.Event == config.EVENT_MSG_DELETE {
return "", nil return "", nil
} }
b.Messages.Enqueue(&msg) b.Messages.Enqueue(&msg)
return "", nil return "", nil
} }
func (b *API) handleHealthcheck(c echo.Context) error { func (b *Api) handlePostMessage(c echo.Context) error {
return c.String(http.StatusOK, "OK") message := &ApiMessage{}
} if err := c.Bind(message); err != nil {
func (b *API) handlePostMessage(c echo.Context) error {
message := config.Message{}
if err := c.Bind(&message); err != nil {
return err return err
} }
// these values are fixed flog.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
message.Channel = "api" b.Remote <- config.Message{
message.Protocol = "api" Text: message.Text,
message.Account = b.Account Username: message.Username,
message.ID = "" UserID: message.UserID,
message.Timestamp = time.Now() Channel: "api",
b.Log.Debugf("Sending message from %s on %s to gateway", message.Username, "api") Avatar: message.Avatar,
b.Remote <- message Account: b.Account,
Gateway: message.Gateway,
Protocol: "api",
}
return c.JSON(http.StatusOK, message) return c.JSON(http.StatusOK, message)
} }
func (b *API) handleMessages(c echo.Context) error { func (b *Api) handleMessages(c echo.Context) error {
b.Lock() b.Lock()
defer b.Unlock() defer b.Unlock()
c.JSONPretty(http.StatusOK, b.Messages.Values(), " ") c.JSONPretty(http.StatusOK, b.Messages.Values(), " ")
@ -106,25 +102,23 @@ func (b *API) handleMessages(c echo.Context) error {
return nil return nil
} }
func (b *API) handleStream(c echo.Context) error { func (b *Api) handleStream(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c.Response().WriteHeader(http.StatusOK) c.Response().WriteHeader(http.StatusOK)
greet := config.Message{ closeNotifier := c.Response().CloseNotify()
Event: config.EventAPIConnected,
Timestamp: time.Now(),
}
if err := json.NewEncoder(c.Response()).Encode(greet); err != nil {
return err
}
c.Response().Flush()
for { for {
msg := b.Messages.Dequeue() select {
if msg != nil { case <-closeNotifier:
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil { return nil
return err default:
msg := b.Messages.Dequeue()
if msg != nil {
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
return err
}
c.Response().Flush()
} }
c.Response().Flush() time.Sleep(200 * time.Millisecond)
} }
time.Sleep(200 * time.Millisecond)
} }
} }

View File

@ -1,11 +1,22 @@
package bridge package bridge
import ( import (
"strings" "github.com/42wim/matterbridge/bridge/api"
"sync"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus" "github.com/42wim/matterbridge/bridge/discord"
"github.com/42wim/matterbridge/bridge/gitter"
"github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/matrix"
"github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/rocketchat"
"github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/sshchat"
"github.com/42wim/matterbridge/bridge/steam"
"github.com/42wim/matterbridge/bridge/telegram"
"github.com/42wim/matterbridge/bridge/xmpp"
log "github.com/Sirupsen/logrus"
"strings"
) )
type Bridger interface { type Bridger interface {
@ -16,59 +27,80 @@ type Bridger interface {
} }
type Bridge struct { type Bridge struct {
Config config.Protocol
Bridger Bridger
*sync.RWMutex Name string
Account string
Name string Protocol string
Account string Channels map[string]config.ChannelInfo
Protocol string Joined map[string]bool
Channels map[string]config.ChannelInfo
Joined map[string]bool
ChannelMembers *config.ChannelMembers
Log *logrus.Entry
Config config.Config
General *config.Protocol
} }
type Config struct { func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge {
*Bridge b := new(Bridge)
b.Channels = make(map[string]config.ChannelInfo)
Remote chan config.Message
}
// Factory is the factory function to create a bridge
type Factory func(*Config) Bridger
func New(bridge *config.Bridge) *Bridge {
accInfo := strings.Split(bridge.Account, ".") accInfo := strings.Split(bridge.Account, ".")
protocol := accInfo[0] protocol := accInfo[0]
name := accInfo[1] name := accInfo[1]
b.Name = name
b.Protocol = protocol
b.Account = bridge.Account
b.Joined = make(map[string]bool)
bridgeConfig := &config.BridgeConfig{General: &cfg.General, Account: bridge.Account, Remote: c}
return &Bridge{ // override config from environment
RWMutex: new(sync.RWMutex), config.OverrideCfgFromEnv(cfg, protocol, name)
Channels: make(map[string]config.ChannelInfo), switch protocol {
Name: name, case "mattermost":
Protocol: protocol, bridgeConfig.Config = cfg.Mattermost[name]
Account: bridge.Account, b.Bridger = bmattermost.New(bridgeConfig)
Joined: make(map[string]bool), case "irc":
bridgeConfig.Config = cfg.IRC[name]
b.Bridger = birc.New(bridgeConfig)
case "gitter":
bridgeConfig.Config = cfg.Gitter[name]
b.Bridger = bgitter.New(bridgeConfig)
case "slack":
bridgeConfig.Config = cfg.Slack[name]
b.Bridger = bslack.New(bridgeConfig)
case "xmpp":
bridgeConfig.Config = cfg.Xmpp[name]
b.Bridger = bxmpp.New(bridgeConfig)
case "discord":
bridgeConfig.Config = cfg.Discord[name]
b.Bridger = bdiscord.New(bridgeConfig)
case "telegram":
bridgeConfig.Config = cfg.Telegram[name]
b.Bridger = btelegram.New(bridgeConfig)
case "rocketchat":
bridgeConfig.Config = cfg.Rocketchat[name]
b.Bridger = brocketchat.New(bridgeConfig)
case "matrix":
bridgeConfig.Config = cfg.Matrix[name]
b.Bridger = bmatrix.New(bridgeConfig)
case "steam":
bridgeConfig.Config = cfg.Steam[name]
b.Bridger = bsteam.New(bridgeConfig)
case "sshchat":
bridgeConfig.Config = cfg.Sshchat[name]
b.Bridger = bsshchat.New(bridgeConfig)
case "api":
bridgeConfig.Config = cfg.Api[name]
b.Bridger = api.New(bridgeConfig)
} }
b.Config = bridgeConfig.Config
return b
} }
func (b *Bridge) JoinChannels() error { func (b *Bridge) JoinChannels() error {
return b.joinChannels(b.Channels, b.Joined) err := b.joinChannels(b.Channels, b.Joined)
} return err
// 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 { func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
for ID, channel := range channels { for ID, channel := range channels {
if !exists[ID] { if !exists[ID] {
b.Log.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID) log.Infof("%s: joining %s (%s)", b.Account, channel.Name, ID)
err := b.JoinChannel(channel) err := b.JoinChannel(channel)
if err != nil { if err != nil {
return err return err
@ -78,43 +110,3 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map
} }
return nil return nil
} }
func (b *Bridge) GetBool(key string) bool {
val, ok := b.Config.GetBool(b.Account + "." + key)
if !ok {
val, _ = b.Config.GetBool("general." + key)
}
return val
}
func (b *Bridge) GetInt(key string) int {
val, ok := b.Config.GetInt(b.Account + "." + key)
if !ok {
val, _ = b.Config.GetInt("general." + key)
}
return val
}
func (b *Bridge) GetString(key string) string {
val, ok := b.Config.GetString(b.Account + "." + key)
if !ok {
val, _ = b.Config.GetString("general." + key)
}
return val
}
func (b *Bridge) GetStringSlice(key string) []string {
val, ok := b.Config.GetStringSlice(b.Account + "." + key)
if !ok {
val, _ = b.Config.GetStringSlice("general." + key)
}
return val
}
func (b *Bridge) GetStringSlice2D(key string) [][]string {
val, ok := b.Config.GetStringSlice2D(b.Account + "." + key)
if !ok {
val, _ = b.Config.GetStringSlice2D("general." + key)
}
return val
}

View File

@ -1,29 +1,20 @@
package config package config
import ( import (
"bytes" "github.com/BurntSushi/toml"
"io/ioutil" "log"
"os"
"reflect"
"strings" "strings"
"sync"
"time" "time"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
) )
const ( const (
EventJoinLeave = "join_leave" EVENT_JOIN_LEAVE = "join_leave"
EventTopicChange = "topic_change" EVENT_FAILURE = "failure"
EventFailure = "failure" EVENT_REJOIN_CHANNELS = "rejoin_channels"
EventFileFailureSize = "file_failure_size" EVENT_USER_ACTION = "user_action"
EventAvatarDownload = "avatar_download" EVENT_MSG_DELETE = "msg_delete"
EventRejoinChannels = "rejoin_channels"
EventUserAction = "user_action"
EventMsgDelete = "msg_delete"
EventAPIConnected = "api_connected"
EventUserTyping = "user_typing"
EventGetChannelMembers = "get_channel_members"
) )
type Message struct { type Message struct {
@ -36,7 +27,6 @@ type Message struct {
Event string `json:"event"` Event string `json:"event"`
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Gateway string `json:"gateway"` Gateway string `json:"gateway"`
ParentID string `json:"parent_id"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
ID string `json:"id"` ID string `json:"id"`
Extra map[string][]interface{} Extra map[string][]interface{}
@ -47,9 +37,6 @@ type FileInfo struct {
Data *[]byte Data *[]byte
Comment string Comment string
URL string URL string
Size int64
Avatar bool
SHA string
} }
type ChannelInfo struct { type ChannelInfo struct {
@ -61,39 +48,21 @@ type ChannelInfo struct {
Options ChannelOptions Options ChannelOptions
} }
type ChannelMember struct {
Username string
Nick string
UserID string
ChannelID string
ChannelName string
}
type ChannelMembers []ChannelMember
type Protocol struct { type Protocol struct {
AuthCode string // steam AuthCode string // steam
BindAddress string // mattermost, slack // DEPRECATED BindAddress string // mattermost, slack // DEPRECATED
Buffer int // api Buffer int // api
Charset string // irc Charset string // irc
ColorNicks bool // only irc for now
Debug bool // general
DebugLevel int // only for irc now
EditSuffix string // mattermost, slack, discord, telegram, gitter EditSuffix string // mattermost, slack, discord, telegram, gitter
EditDisable bool // mattermost, slack, discord, telegram, gitter EditDisable bool // mattermost, slack, discord, telegram, gitter
IconURL string // mattermost, slack IconURL string // mattermost, slack
IgnoreFailureOnStart bool // general
IgnoreNicks string // all protocols IgnoreNicks string // all protocols
IgnoreMessages string // all protocols IgnoreMessages string // all protocols
Jid string // xmpp Jid string // xmpp
Label string // all protocols
Login string // mattermost, matrix Login string // mattermost, matrix
MediaDownloadBlackList []string
MediaDownloadPath string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server.
MediaDownloadSize int // all protocols MediaDownloadSize int // all protocols
MediaServerDownload string MediaServerDownload string
MediaServerUpload string MediaServerUpload string
MediaConvertWebPToPNG bool // telegram
MessageDelay int // IRC, time in millisecond to wait between messages MessageDelay int // IRC, time in millisecond to wait between messages
MessageFormat string // telegram MessageFormat string // telegram
MessageLength int // IRC, max length of a message allowed MessageLength int // IRC, max length of a message allowed
@ -108,47 +77,36 @@ type Protocol struct {
NickServPassword string // IRC NickServPassword string // IRC
NicksPerRow int // mattermost, slack NicksPerRow int // mattermost, slack
NoHomeServerSuffix bool // matrix NoHomeServerSuffix bool // matrix
NoSendJoinPart bool // all protocols
NoTLS bool // mattermost NoTLS bool // mattermost
Password string // IRC,mattermost,XMPP,matrix Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack PrefixMessagesWithNick bool // mattemost, slack
PreserveThreading bool // slack
Protocol string // all protocols Protocol string // all protocols
QuoteDisable bool // telegram
QuoteFormat string // telegram
RejoinDelay int // IRC RejoinDelay int // IRC
ReplaceMessages [][]string // all protocols ReplaceMessages [][]string // all protocols
ReplaceNicks [][]string // all protocols ReplaceNicks [][]string // all protocols
RemoteNickFormat string // all protocols RemoteNickFormat string // all protocols
RunCommands []string // irc
Server string // IRC,mattermost,XMPP,discord Server string // IRC,mattermost,XMPP,discord
ShowJoinPart bool // all protocols ShowJoinPart bool // all protocols
ShowTopicChange bool // slack
ShowUserTyping bool // slack
ShowEmbeds bool // discord ShowEmbeds bool // discord
SkipTLSVerify bool // IRC, mattermost SkipTLSVerify bool // IRC, mattermost
StripNick bool // all protocols StripNick bool // all protocols
SyncTopic bool // slack
TengoModifyMessage string // general
Team string // mattermost Team string // mattermost
Token string // gitter, slack, discord, api Token string // gitter, slack, discord, api
Topic string // zulip
URL string // mattermost, slack // DEPRECATED URL string // mattermost, slack // DEPRECATED
UseAPI bool // mattermost, slack UseAPI bool // mattermost, slack
UseSASL bool // IRC UseSASL bool // IRC
UseTLS bool // IRC UseTLS bool // IRC
UseDiscriminator bool // discord
UseFirstName bool // telegram UseFirstName bool // telegram
UseUserName bool // discord UseUserName bool // discord
UseInsecureURL bool // telegram UseInsecureURL bool // telegram
WebhookBindAddress string // mattermost, slack WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack WebhookURL string // mattermost, slack
WebhookUse string // mattermost, slack, discord
} }
type ChannelOptions struct { type ChannelOptions struct {
Key string // irc, xmpp Key string // irc
WebhookURL string // discord WebhookURL string // discord
Topic string // zulip
} }
type Bridge struct { type Bridge struct {
@ -173,140 +131,103 @@ type SameChannelGateway struct {
Accounts []string Accounts []string
} }
type BridgeValues struct { type Config struct {
API map[string]Protocol Api map[string]Protocol
IRC map[string]Protocol IRC map[string]Protocol
Mattermost map[string]Protocol Mattermost map[string]Protocol
Matrix map[string]Protocol Matrix map[string]Protocol
Slack map[string]Protocol Slack map[string]Protocol
SlackLegacy map[string]Protocol
Steam map[string]Protocol Steam map[string]Protocol
Gitter map[string]Protocol Gitter map[string]Protocol
XMPP map[string]Protocol Xmpp map[string]Protocol
Discord map[string]Protocol Discord map[string]Protocol
Telegram map[string]Protocol Telegram map[string]Protocol
Rocketchat map[string]Protocol Rocketchat map[string]Protocol
SSHChat map[string]Protocol 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 General Protocol
Gateway []Gateway Gateway []Gateway
SameChannelGateway []SameChannelGateway SameChannelGateway []SameChannelGateway
} }
type Config interface { type BridgeConfig struct {
BridgeValues() *BridgeValues Config Protocol
GetBool(key string) (bool, bool) General *Protocol
GetInt(key string) (int, bool) Account string
GetString(key string) (string, bool) Remote chan Message
GetStringSlice(key string) ([]string, bool)
GetStringSlice2D(key string) ([][]string, bool)
} }
type config struct { func NewConfig(cfgfile string) *Config {
sync.RWMutex var cfg Config
if _, err := toml.DecodeFile(cfgfile, &cfg); err != nil {
logger *logrus.Entry log.Fatal(err)
v *viper.Viper
cv *BridgeValues
}
// 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 := ioutil.ReadFile(cfgfile)
if err != nil {
logger.Fatalf("Failed to read configuration file: %#v", err)
} }
fail := false
mycfg := newConfigFromString(logger, input) for k, v := range cfg.Mattermost {
if mycfg.cv.General.MediaDownloadSize == 0 { res := Deprecated(v, "mattermost."+k)
mycfg.cv.General.MediaDownloadSize = 1000000 if res {
} fail = res
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
logger.Println("Config file changed:", e.Name)
})
return mycfg
}
// 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(logger *logrus.Entry, input []byte) *config {
viper.SetConfigType("toml")
viper.SetEnvPrefix("matterbridge")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()
if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil {
logger.Fatalf("Failed to parse the configuration: %#v", err)
}
cfg := &BridgeValues{}
if err := viper.Unmarshal(cfg); err != nil {
logger.Fatalf("Failed to load the configuration: %#v", err)
}
return &config{
logger: logger,
v: viper.GetViper(),
cv: cfg,
}
}
func (c *config) BridgeValues() *BridgeValues {
return c.cv
}
func (c *config) GetBool(key string) (bool, bool) {
c.RLock()
defer c.RUnlock()
return c.v.GetBool(key), c.v.IsSet(key)
}
func (c *config) GetInt(key string) (int, bool) {
c.RLock()
defer c.RUnlock()
return c.v.GetInt(key), c.v.IsSet(key)
}
func (c *config) GetString(key string) (string, bool) {
c.RLock()
defer c.RUnlock()
return c.v.GetString(key), c.v.IsSet(key)
}
func (c *config) GetStringSlice(key string) ([]string, bool) {
c.RLock()
defer c.RUnlock()
return c.v.GetStringSlice(key), c.v.IsSet(key)
}
func (c *config) GetStringSlice2D(key string) ([][]string, bool) {
c.RLock()
defer c.RUnlock()
res, ok := c.v.Get(key).([]interface{})
if !ok {
return nil, 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 for k, v := range cfg.Slack {
res := Deprecated(v, "slack."+k)
if res {
fail = res
}
}
for k, v := range cfg.Rocketchat {
res := Deprecated(v, "rocketchat."+k)
if res {
fail = res
}
}
if fail {
log.Fatalf("Fix your config. Please see changelog for more information")
}
if cfg.General.MediaDownloadSize == 0 {
cfg.General.MediaDownloadSize = 1000000
}
return &cfg
} }
func GetIconURL(msg *Message, iconURL string) string { func OverrideCfgFromEnv(cfg *Config, protocol string, account string) {
var protoCfg Protocol
val := reflect.ValueOf(cfg).Elem()
// loop over the Config struct
for i := 0; i < val.NumField(); i++ {
typeField := val.Type().Field(i)
// look for the protocol map (both lowercase)
if strings.ToLower(typeField.Name) == protocol {
// get the Protocol struct from the map
data := val.Field(i).MapIndex(reflect.ValueOf(account))
protoCfg = data.Interface().(Protocol)
protoStruct := reflect.ValueOf(&protoCfg).Elem()
// loop over the found protocol struct
for i := 0; i < protoStruct.NumField(); i++ {
typeField := protoStruct.Type().Field(i)
// build our environment key (eg MATTERBRIDGE_MATTERMOST_WORK_LOGIN)
key := "matterbridge_" + protocol + "_" + account + "_" + typeField.Name
key = strings.ToUpper(key)
// search the environment
res := os.Getenv(key)
// if it exists and the current field is a string
// then update the current field
if res != "" {
fieldVal := protoStruct.Field(i)
if fieldVal.Kind() == reflect.String {
log.Printf("config: overriding %s from env with %s\n", key, res)
fieldVal.Set(reflect.ValueOf(res))
}
}
}
// update the map with the modified Protocol (cfg.Protocol[account] = Protocol)
val.Field(i).SetMapIndex(reflect.ValueOf(account), reflect.ValueOf(protoCfg))
break
}
}
}
func GetIconURL(msg *Message, cfg *Protocol) string {
iconURL := cfg.IconURL
info := strings.Split(msg.Account, ".") info := strings.Split(msg.Account, ".")
protocol := info[0] protocol := info[0]
name := info[1] name := info[1]
@ -316,44 +237,16 @@ func GetIconURL(msg *Message, iconURL string) string {
return iconURL return iconURL
} }
type TestConfig struct { func Deprecated(cfg Protocol, account string) bool {
Config if cfg.BindAddress != "" {
log.Printf("ERROR: %s BindAddress is deprecated, you need to change it to WebhookBindAddress.", account)
Overrides map[string]interface{} } else if cfg.URL != "" {
} log.Printf("ERROR: %s URL is deprecated, you need to change it to WebhookURL.", account)
} else if cfg.UseAPI {
func (c *TestConfig) GetBool(key string) (bool, bool) { log.Printf("ERROR: %s UseAPI is deprecated, it's enabled by default, please remove it from your config file.", account)
val, ok := c.Overrides[key] } else {
if ok { return false
return val.(bool), true
} }
return c.Config.GetBool(key) return true
} //log.Fatalf("ERROR: Fix your config: %s", account)
func (c *TestConfig) GetInt(key string) (int, bool) {
if val, ok := c.Overrides[key]; ok {
return val.(int), true
}
return c.Config.GetInt(key)
}
func (c *TestConfig) GetString(key string) (string, bool) {
if val, ok := c.Overrides[key]; ok {
return val.(string), true
}
return c.Config.GetString(key)
}
func (c *TestConfig) GetStringSlice(key string) ([]string, bool) {
if val, ok := c.Overrides[key]; ok {
return val.([]string), true
}
return c.Config.GetStringSlice(key)
}
func (c *TestConfig) GetStringSlice2D(key string) ([][]string, bool) {
if val, ok := c.Overrides[key]; ok {
return val.([][]string), true
}
return c.Config.GetStringSlice2D(key)
} }

View File

@ -2,317 +2,377 @@ package bdiscord
import ( import (
"bytes" "bytes"
"errors" "github.com/42wim/matterbridge/bridge/config"
"fmt" log "github.com/Sirupsen/logrus"
"github.com/bwmarrin/discordgo"
"regexp"
"strings" "strings"
"sync" "sync"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/bwmarrin/discordgo"
) )
const MessageLength = 1950 type bdiscord struct {
c *discordgo.Session
type Bdiscord struct { Channels []*discordgo.Channel
*bridge.Config Nick string
UseChannelID bool
c *discordgo.Session userMemberMap map[string]*discordgo.Member
guildID string
nick string webhookID string
useChannelID bool webhookToken string
guildID string
webhookID string
webhookToken string
canEditWebhooks bool
channelsMutex sync.RWMutex
channels []*discordgo.Channel
channelInfoMap map[string]*config.ChannelInfo channelInfoMap map[string]*config.ChannelInfo
sync.RWMutex
membersMutex sync.RWMutex *config.BridgeConfig
userMemberMap map[string]*discordgo.Member
nickMemberMap map[string]*discordgo.Member
} }
func New(cfg *bridge.Config) bridge.Bridger { var flog *log.Entry
b := &Bdiscord{Config: cfg} var protocol = "discord"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *bdiscord {
b := &bdiscord{BridgeConfig: cfg}
b.userMemberMap = make(map[string]*discordgo.Member) b.userMemberMap = make(map[string]*discordgo.Member)
b.nickMemberMap = make(map[string]*discordgo.Member)
b.channelInfoMap = make(map[string]*config.ChannelInfo) b.channelInfoMap = make(map[string]*config.ChannelInfo)
if b.GetString("WebhookURL") != "" { if b.Config.WebhookURL != "" {
b.Log.Debug("Configuring Discord Incoming Webhook") flog.Debug("Configuring Discord Incoming Webhook")
b.webhookID, b.webhookToken = b.splitURL(b.GetString("WebhookURL")) b.webhookID, b.webhookToken = b.splitURL(b.Config.WebhookURL)
} }
return b return b
} }
func (b *Bdiscord) Connect() error { func (b *bdiscord) Connect() error {
var err error var err error
var guildFound bool flog.Info("Connecting")
token := b.GetString("Token") if b.Config.WebhookURL == "" {
b.Log.Info("Connecting") flog.Info("Connecting using token")
if b.GetString("WebhookURL") == "" {
b.Log.Info("Connecting using token")
} else { } else {
b.Log.Info("Connecting using webhookurl (for posting) and token") flog.Info("Connecting using webhookurl (for posting) and token")
} }
if !strings.HasPrefix(b.GetString("Token"), "Bot ") { if !strings.HasPrefix(b.Config.Token, "Bot ") {
token = "Bot " + b.GetString("Token") b.Config.Token = "Bot " + b.Config.Token
} }
// if we have a User token, remove the `Bot` prefix b.c, err = discordgo.New(b.Config.Token)
if strings.HasPrefix(b.GetString("Token"), "User ") {
token = strings.Replace(b.GetString("Token"), "User ", "", -1)
}
b.c, err = discordgo.New(token)
if err != nil { if err != nil {
flog.Debugf("%#v", err)
return err return err
} }
b.Log.Info("Connection succeeded") flog.Info("Connection succeeded")
b.c.AddHandler(b.messageCreate) b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.memberUpdate) b.c.AddHandler(b.memberUpdate)
b.c.AddHandler(b.messageUpdate) b.c.AddHandler(b.messageUpdate)
b.c.AddHandler(b.messageDelete) b.c.AddHandler(b.messageDelete)
b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove)
err = b.c.Open() err = b.c.Open()
if err != nil { if err != nil {
flog.Debugf("%#v", err)
return err return err
} }
guilds, err := b.c.UserGuilds(100, "", "") guilds, err := b.c.UserGuilds(100, "", "")
if err != nil { if err != nil {
flog.Debugf("%#v", err)
return err return err
} }
userinfo, err := b.c.User("@me") userinfo, err := b.c.User("@me")
if err != nil { if err != nil {
flog.Debugf("%#v", err)
return err return err
} }
serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1) b.Nick = userinfo.Username
b.nick = userinfo.Username
b.channelsMutex.Lock()
for _, guild := range guilds { for _, guild := range guilds {
if guild.Name == serverName || guild.ID == serverName { if guild.Name == b.Config.Server {
b.channels, err = b.c.GuildChannels(guild.ID) b.Channels, err = b.c.GuildChannels(guild.ID)
b.guildID = guild.ID b.guildID = guild.ID
guildFound = true
if err != nil { if err != nil {
break flog.Debugf("%#v", err)
return err
} }
} }
} }
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)
}
}
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 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 != "" {
b.nickMemberMap[member.Nick] = member
}
}
return nil return nil
} }
func (b *Bdiscord) Disconnect() error { func (b *bdiscord) Disconnect() error {
return b.c.Close() return nil
} }
func (b *Bdiscord) JoinChannel(channel config.ChannelInfo) error { func (b *bdiscord) JoinChannel(channel config.ChannelInfo) error {
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelInfoMap[channel.ID] = &channel b.channelInfoMap[channel.ID] = &channel
idcheck := strings.Split(channel.Name, "ID:") idcheck := strings.Split(channel.Name, "ID:")
if len(idcheck) > 1 { if len(idcheck) > 1 {
b.useChannelID = true b.UseChannelID = true
} }
return nil return nil
} }
func (b *Bdiscord) Send(msg config.Message) (string, error) { func (b *bdiscord) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
channelID := b.getChannelID(msg.Channel) channelID := b.getChannelID(msg.Channel)
if channelID == "" { if channelID == "" {
return "", fmt.Errorf("Could not find channelID for %v", msg.Channel) flog.Errorf("Could not find channelID for %v", msg.Channel)
return "", nil
} }
if msg.Event == config.EVENT_USER_ACTION {
// Make a action /me of the message
if msg.Event == config.EventUserAction {
msg.Text = "_" + msg.Text + "_" msg.Text = "_" + msg.Text + "_"
} }
// use initial webhook configured for the entire Discord account
isGlobalWebhook := true
wID := b.webhookID wID := b.webhookID
wToken := b.webhookToken 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, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
if ci.Options.WebhookURL != "" { if ci.Options.WebhookURL != "" {
wID, wToken = b.splitURL(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 != "" { flog.Debugf("Broadcasting using token (API)")
// skip events if msg.Event == config.EVENT_MSG_DELETE {
if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange { if msg.ID == "" {
return "", nil return "", nil
}
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 != "" { err := b.c.ChannelMessageDelete(channelID, msg.ID)
msg.Text = fi.URL return "", err
if fi.Comment != "" { }
msg.Text = fi.Comment + ": " + fi.URL if msg.ID != "" {
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
return msg.ID, err
}
if msg.Extra != nil {
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
var err error
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
files := []*discordgo.File{}
files = append(files, &discordgo.File{fi.Name, "", bytes.NewReader(*fi.Data)})
_, err = b.c.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{Content: msg.Username + fi.Comment, Files: files})
if err != nil {
flog.Errorf("file upload failed: %#v", err)
}
} }
return "", nil
} }
} }
// skip empty messages
if msg.Text == "" {
return "", nil
}
msg.Text = helper.ClipMessage(msg.Text, MessageLength) res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
msg.Text = b.replaceUserMentions(msg.Text) if err != nil {
// discord username must be [0..32] max return "", err
if len(msg.Username) > 32 {
msg.Username = msg.Username[0:32]
} }
// if we have a global webhook for this Discord account, and permission return res.ID, err
// to modify webhooks (previously verified), then set its channel to }
// the message channel before using it flog.Debugf("Broadcasting using Webhook")
// TODO: this isn't necessary if the last message from this webhook was err := b.c.WebhookExecute(
// sent to the current channel wID,
if isGlobalWebhook && b.canEditWebhooks { wToken,
b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel) true,
_, err := b.c.WebhookEdit(wID, "", "", channelID) &discordgo.WebhookParams{
if err != nil { Content: msg.Text,
b.Log.Errorf("Could not set webhook channel: %s", err) Username: msg.Username,
return "", err AvatarURL: msg.Avatar,
})
return "", err
}
func (b *bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) {
rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EVENT_MSG_DELETE, Text: config.EVENT_MSG_DELETE}
rmsg.Channel = b.getChannelName(m.ChannelID)
if b.UseChannelID {
rmsg.Channel = "ID:" + m.ChannelID
}
flog.Debugf("Sending message from %s to gateway", b.Account)
flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
if b.Config.EditDisable {
return
}
// only when message is actually edited
if m.Message.EditedTimestamp != "" {
flog.Debugf("Sending edit message")
m.Content = m.Content + b.Config.EditSuffix
b.messageCreate(s, (*discordgo.MessageCreate)(m))
}
}
func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// 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
}
if len(m.Attachments) > 0 {
for _, attach := range m.Attachments {
m.Content = m.Content + "\n" + attach.URL
}
}
var text string
if m.Content != "" {
flog.Debugf("Receiving message %#v", m.Message)
if len(m.MentionRoles) > 0 {
m.Message.Content = b.replaceRoleMentions(m.Message.Content)
}
m.Message.Content = b.stripCustomoji(m.Message.Content)
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
text = m.ContentWithMentionsReplaced()
}
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}
rmsg.Channel = b.getChannelName(m.ChannelID)
if b.UseChannelID {
rmsg.Channel = "ID:" + m.ChannelID
}
if !b.Config.UseUserName {
rmsg.Username = b.getNick(m.Author)
} else {
rmsg.Username = m.Author.Username
}
if b.Config.ShowEmbeds && m.Message.Embeds != nil {
for _, embed := range m.Message.Embeds {
text = text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n"
}
}
// no empty messages
if text == "" {
return
}
text, ok := b.replaceAction(text)
if ok {
rmsg.Event = config.EVENT_USER_ACTION
}
rmsg.Text = text
flog.Debugf("Sending message from %s on %s to gateway", m.Author.Username, b.Account)
flog.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 {
flog.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.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
err := b.c.WebhookExecute( return user.Username
wID,
wToken,
true,
&discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
})
return "", err
}
b.Log.Debugf("Broadcasting using token (API)")
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
err := b.c.ChannelMessageDelete(channelID, msg.ID)
return "", err
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength)
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 {
return b.handleUploadFile(&msg, channelID)
} }
} }
// if we didn't find nick, search for it
msg.Text = helper.ClipMessage(msg.Text, MessageLength) member, err := b.c.GuildMember(b.guildID, user.ID)
msg.Text = b.replaceUserMentions(msg.Text)
// Edit message
if msg.ID != "" {
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
return msg.ID, err
}
// Post normal message
res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
if err != nil { if err != nil {
return "", err return user.Username
} }
return res.ID, err 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) 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) replaceRoleMentions(text string) string {
roles, err := b.c.GuildRoles(b.guildID)
if err != nil {
flog.Debugf("%#v", string(err.(*discordgo.RESTError).ResponseBody))
return text
}
for _, role := range roles {
text = strings.Replace(text, "<@&"+role.ID+">", "@"+role.Name, -1)
}
return text
}
func (b *bdiscord) replaceChannelMentions(text string) string {
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) 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, "/")
return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1]
} }
// useWebhook returns true if we have a webhook defined somewhere // useWebhook returns true if we have a webhook defined somewhere
func (b *Bdiscord) useWebhook() bool { func (b *bdiscord) useWebhook() bool {
if b.GetString("WebhookURL") != "" { if b.Config.WebhookURL != "" {
return true return true
} }
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channelInfoMap { for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" { if channel.Options.WebhookURL != "" {
return true return true
@ -322,17 +382,13 @@ func (b *Bdiscord) useWebhook() bool {
} }
// isWebhookID returns true if the specified id is used in a defined webhook // isWebhookID returns true if the specified id is used in a defined webhook
func (b *Bdiscord) isWebhookID(id string) bool { func (b *bdiscord) isWebhookID(id string) bool {
if b.GetString("WebhookURL") != "" { if b.Config.WebhookURL != "" {
wID, _ := b.splitURL(b.GetString("WebhookURL")) wID, _ := b.splitURL(b.Config.WebhookURL)
if wID == id { if wID == id {
return true return true
} }
} }
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channelInfoMap { for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" { if channel.Options.WebhookURL != "" {
wID, _ := b.splitURL(channel.Options.WebhookURL) wID, _ := b.splitURL(channel.Options.WebhookURL)
@ -343,25 +399,3 @@ func (b *Bdiscord) isWebhookID(id string) bool {
} }
return false return false
} }
// handleUploadFile handles native upload of files
func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) {
var err error
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := discordgo.File{
Name: fi.Name,
ContentType: "",
Reader: bytes.NewReader(*fi.Data),
}
m := discordgo.MessageSend{
Content: msg.Username + fi.Comment,
Files: []*discordgo.File{&file},
}
_, err = b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil {
return "", fmt.Errorf("file upload failed: %s", err)
}
}
return "", nil
}

View File

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

View File

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

View File

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

View File

@ -2,12 +2,10 @@ package bgitter
import ( import (
"fmt" "fmt"
"strings"
"github.com/42wim/go-gitter" "github.com/42wim/go-gitter"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" log "github.com/Sirupsen/logrus"
"strings"
) )
type Bgitter struct { type Bgitter struct {
@ -15,26 +13,31 @@ type Bgitter struct {
User *gitter.User User *gitter.User
Users []gitter.User Users []gitter.User
Rooms []gitter.Room Rooms []gitter.Room
*bridge.Config *config.BridgeConfig
} }
func New(cfg *bridge.Config) bridge.Bridger { var flog *log.Entry
return &Bgitter{Config: cfg} var protocol = "gitter"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bgitter {
return &Bgitter{BridgeConfig: cfg}
} }
func (b *Bgitter) Connect() error { func (b *Bgitter) Connect() error {
var err error var err error
b.Log.Info("Connecting") flog.Info("Connecting")
b.c = gitter.New(b.GetString("Token")) b.c = gitter.New(b.Config.Token)
b.User, err = b.c.GetUser() b.User, err = b.c.GetUser()
if err != nil { if err != nil {
flog.Debugf("%#v", err)
return err return err
} }
b.Rooms, err = b.c.GetRooms() flog.Info("Connection succeeded")
if err != nil { b.Rooms, _ = b.c.GetRooms()
return err
}
b.Log.Info("Connection succeeded")
return nil return nil
} }
@ -70,21 +73,20 @@ func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
for event := range stream.Event { for event := range stream.Event {
switch ev := event.Data.(type) { switch ev := event.Data.(type) {
case *gitter.MessageReceived: case *gitter.MessageReceived:
// ignore message sent from ourselves
if ev.Message.From.ID != b.User.ID { if ev.Message.From.ID != b.User.ID {
b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account) flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room, rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID, Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID,
ID: ev.Message.ID} ID: ev.Message.ID}
if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) { if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) {
rmsg.Event = config.EventUserAction rmsg.Event = config.EVENT_USER_ACTION
rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1) rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1)
} }
b.Log.Debugf("<= Message is %#v", rmsg) flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
} }
case *gitter.GitterConnectionClosed: case *gitter.GitterConnectionClosed:
b.Log.Errorf("connection with gitter closed for room %s", room) flog.Errorf("connection with gitter closed for room %s", room)
} }
} }
}(stream, room.URI) }(stream, room.URI)
@ -92,39 +94,25 @@ func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
} }
func (b *Bgitter) Send(msg config.Message) (string, error) { func (b *Bgitter) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
roomID := b.getRoomID(msg.Channel) roomID := b.getRoomID(msg.Channel)
if roomID == "" { if roomID == "" {
b.Log.Errorf("Could not find roomID for %v", msg.Channel) flog.Errorf("Could not find roomID for %v", msg.Channel)
return "", nil return "", nil
} }
if msg.Event == config.EVENT_MSG_DELETE {
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" { if msg.ID == "" {
return "", nil return "", nil
} }
// gitter has no delete message api so we edit message to "" // gitter has no delete message api
_, err := b.c.UpdateMessage(roomID, msg.ID, "") _, err := b.c.UpdateMessage(roomID, msg.ID, "")
if err != nil { if err != nil {
return "", err return "", err
} }
return "", nil return "", nil
} }
// Upload a file (in gitter case send the upload URL because gitter has no native upload support)
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)
}
}
// Edit message
if msg.ID != "" { if msg.ID != "" {
b.Log.Debugf("updating message with id %s", msg.ID) flog.Debugf("updating message with id %s", msg.ID)
_, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text) _, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text)
if err != nil { if err != nil {
return "", err return "", err
@ -132,7 +120,22 @@ func (b *Bgitter) Send(msg config.Message) (string, error) {
return "", nil return "", nil
} }
// Post normal message if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text = fi.URL
}
_, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
if err != nil {
return "", err
}
}
return "", nil
}
}
resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text) resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
if err != nil { if err != nil {
return "", err return "", err
@ -160,23 +163,3 @@ func (b *Bgitter) getAvatar(user string) string {
} }
return avatar return avatar
} }
func (b *Bgitter) handleUploadFile(msg *config.Message, roomID string) (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
}
}
_, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
if err != nil {
return "", err
}
}
return "", nil
}

View File

@ -2,197 +2,39 @@ package helper
import ( import (
"bytes" "bytes"
"fmt"
"image/png"
"io" "io"
"net/http" "net/http"
"regexp"
"strings"
"time" "time"
"unicode/utf8"
"golang.org/x/image/webp"
"github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus"
"gitlab.com/golang-commonmark/markdown"
) )
// DownloadFile downloads the given non-authenticated URL.
func DownloadFile(url string) (*[]byte, error) { 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 var buf bytes.Buffer
client := &http.Client{ client := &http.Client{
Timeout: time.Second * 5, Timeout: time.Second * 5,
} }
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if auth != "" {
req.Header.Add("Authorization", auth)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
resp.Body.Close()
return nil, err return nil, err
} }
defer resp.Body.Close()
io.Copy(&buf, resp.Body) io.Copy(&buf, resp.Body)
data := buf.Bytes() data := buf.Bytes()
resp.Body.Close()
return &data, nil return &data, nil
} }
// GetSubLines splits messages in newline-delimited lines. If maxLineLength is func SplitStringLength(input string, length int) string {
// specified as non-zero GetSubLines will also clip long lines to the maximum a := []rune(input)
// length and insert a warning marker that the line was clipped. str := ""
// for i, r := range a {
// TODO: The current implementation has the inconvenient that it disregards str = str + string(r)
// word boundaries when splitting but this is hard to solve without potentially if i > 0 && (i+1)%length == 0 {
// breaking formatting and other stylistic effects. str += "\n"
func GetSubLines(message string, maxLineLength int) []string {
const clippingMessage = " <clipped message>"
var lines []string
for _, line := range strings.Split(strings.TrimSpace(message), "\n") {
if maxLineLength == 0 || len([]byte(line)) <= maxLineLength {
lines = append(lines, line)
continue
}
// !!! WARNING !!!
// Before touching the splitting logic below please ensure that you PROPERLY
// understand how strings, runes and range loops over strings work in Go.
// A good place to start is to read https://blog.golang.org/strings. :-)
var splitStart int
var startOfPreviousRune int
for i := range line {
if i-splitStart > maxLineLength-len([]byte(clippingMessage)) {
lines = append(lines, line[splitStart:startOfPreviousRune]+clippingMessage)
splitStart = startOfPreviousRune
}
startOfPreviousRune = i
}
// This last append is safe to do without looking at the remaining byte-length
// as we assume that the byte-length of the last rune will never exceed that of
// the byte-length of the clipping message.
lines = append(lines, line[splitStart:])
}
return lines
}
// 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,
})
}
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"
}
return ""
}
// 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 {
logger.Errorf("incorrect regexp %s for %s", entry, msg.Account)
continue
}
if re.MatchString(name) {
return fmt.Errorf("Matching blacklist %s. Not downloading %s", entry, name)
}
} }
} }
logger.Debugf("Trying to download %#v with size %#v", name, size) return str
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,
})
return fmt.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, general.MediaDownloadSize)
}
return nil
}
// 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
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,
})
}
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 {
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 {
const clippingMessage = " <clipped message>"
if len(text) > length {
text = text[:length-len(clippingMessage)]
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
text = text[:len(text)-size]
}
text += clippingMessage
}
return text
}
func ParseMarkdown(input string) string {
md := markdown.New(markdown.XHTMLOutput(true), markdown.Breaks(true))
return (md.RenderToString([]byte(input)))
}
// ConvertWebPToPNG convert input data (which should be WebP format to PNG format)
func ConvertWebPToPNG(data *[]byte) error {
r := bytes.NewReader(*data)
m, err := webp.Decode(r)
if err != nil {
return err
}
var output []byte
w := bytes.NewBuffer(output)
if err := png.Encode(w, m); err != nil {
return err
}
*data = w.Bytes()
return nil
} }

View File

@ -1,126 +0,0 @@
package helper
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
const testLineLength = 64
var (
lineSplittingTestCases = map[string]struct {
input string
splitOutput []string
nonSplitOutput []string
}{
"Short single-line message": {
input: "short",
splitOutput: []string{"short"},
nonSplitOutput: []string{"short"},
},
"Long single-line message": {
input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
splitOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
" labore et dolore magna aliqua.",
},
nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."},
},
"Short multi-line message": {
input: "I\ncan't\nget\nno\nsatisfaction!",
splitOutput: []string{
"I",
"can't",
"get",
"no",
"satisfaction!",
},
nonSplitOutput: []string{
"I",
"can't",
"get",
"no",
"satisfaction!",
},
},
"Long multi-line message": {
input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" +
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
splitOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
" labore et dolore magna aliqua.",
"Ut enim ad minim veniam, quis nostrud exercita <clipped message>",
"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>",
"modo consequat.",
"Duis aute irure dolor in reprehenderit in volu <clipped message>",
"ptate velit esse cillum dolore eu fugiat nulla <clipped message>",
" pariatur.",
"Excepteur sint occaecat cupidatat non proident <clipped message>",
", sunt in culpa qui officia deserunt mollit an <clipped message>",
"im id est laborum.",
},
nonSplitOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
},
},
"Message ending with new-line.": {
input: "Newline ending\n",
splitOutput: []string{"Newline ending"},
nonSplitOutput: []string{"Newline ending"},
},
"Long message containing UTF-8 multi-byte runes": {
input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說",
splitOutput: []string{
"不布人個我此而及單石業喜資富下 <clipped message>",
"我河下日沒一我臺空達的常景便物 <clipped message>",
"沒為……子大我別名解成?生賣的 <clipped message>",
"全直黑,我自我結毛分洲了世當, <clipped message>",
"是政福那是東;斯說",
},
nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
},
}
)
func TestGetSubLines(t *testing.T) {
for testname, testcase := range lineSplittingTestCases {
splitLines := GetSubLines(testcase.input, testLineLength)
assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname)
for _, splitLine := range splitLines {
byteLength := len([]byte(splitLine))
assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength)
}
nonSplitLines := GetSubLines(testcase.input, 0)
assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname)
}
}
func TestConvertWebPToPNG(t *testing.T) {
if os.Getenv("LOCAL_TEST") == "" {
t.Skip()
}
input, err := ioutil.ReadFile("test.webp")
if err != nil {
t.Fail()
}
d := &input
err = ConvertWebPToPNG(d)
if err != nil {
t.Fail()
}
err = ioutil.WriteFile("test.png", *d, 0644)
if err != nil {
t.Fail()
}
}

View File

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

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

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

View File

@ -1,235 +1,86 @@
package birc package birc
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"hash/crc32" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
"github.com/lrstanley/girc"
"github.com/paulrosania/go-charset/charset"
_ "github.com/paulrosania/go-charset/data"
"github.com/saintfish/chardet"
"io"
"io/ioutil"
"net" "net"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/lrstanley/girc"
// 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"
) )
type Birc struct { type Birc struct {
i *girc.Client i *girc.Client
Nick string Nick string
names map[string][]string names map[string][]string
connected chan error connected chan struct{}
Local chan config.Message // local queue for flood control Local chan config.Message // local queue for flood control
FirstConnection, authDone bool FirstConnection bool
MessageDelay, MessageQueue, MessageLength int
*bridge.Config *config.BridgeConfig
} }
func New(cfg *bridge.Config) bridge.Bridger { var flog *log.Entry
var protocol = "irc"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Birc {
b := &Birc{} b := &Birc{}
b.Config = cfg b.BridgeConfig = cfg
b.Nick = b.GetString("Nick") b.Nick = b.Config.Nick
b.names = make(map[string][]string) b.names = make(map[string][]string)
b.connected = make(chan error) b.connected = make(chan struct{})
if b.GetInt("MessageDelay") == 0 { if b.Config.MessageDelay == 0 {
b.MessageDelay = 1300 b.Config.MessageDelay = 1300
} else {
b.MessageDelay = b.GetInt("MessageDelay")
} }
if b.GetInt("MessageQueue") == 0 { if b.Config.MessageQueue == 0 {
b.MessageQueue = 30 b.Config.MessageQueue = 30
} else {
b.MessageQueue = b.GetInt("MessageQueue")
} }
if b.GetInt("MessageLength") == 0 { if b.Config.MessageLength == 0 {
b.MessageLength = 400 b.Config.MessageLength = 400
} else {
b.MessageLength = b.GetInt("MessageLength")
} }
b.FirstConnection = true b.FirstConnection = true
return b return b
} }
func (b *Birc) Command(msg *config.Message) string { func (b *Birc) Command(msg *config.Message) string {
if msg.Text == "!users" { switch msg.Text {
case "!users":
b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames) b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames)
b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames) b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames)
b.i.Cmd.SendRaw("NAMES " + msg.Channel) //nolint:errcheck b.i.Cmd.SendRaw("NAMES " + msg.Channel)
} }
return "" return ""
} }
func (b *Birc) Connect() error { func (b *Birc) Connect() error {
b.Local = make(chan config.Message, b.MessageQueue+10) b.Local = make(chan config.Message, b.Config.MessageQueue+10)
b.Log.Infof("Connecting %s", b.GetString("Server")) flog.Infof("Connecting %s", b.Config.Server)
server, portstr, err := net.SplitHostPort(b.Config.Server)
i, err := b.getClient()
if err != nil { if err != nil {
return err return err
} }
if b.GetBool("UseSASL") {
i.Config.SASL = &girc.SASLPlain{
User: b.GetString("NickServNick"),
Pass: b.GetString("NickServPassword"),
}
}
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)
b.i = i
go b.doConnect()
err = <-b.connected
if err != nil {
return fmt.Errorf("connection failed %s", err)
}
b.Log.Info("Connection succeeded")
b.FirstConnection = false
if b.GetInt("DebugLevel") == 0 {
i.Handlers.Clear(girc.ALL_EVENTS)
}
go b.doSend()
return nil
}
func (b *Birc) Disconnect() error {
b.i.Close()
close(b.Local)
return nil
}
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
// need to check if we have nickserv auth done before joining channels
for {
if b.authDone {
break
}
time.Sleep(time.Second)
}
if channel.Options.Key != "" {
b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
b.i.Cmd.JoinKey(channel.Name, channel.Options.Key)
} else {
b.i.Cmd.Join(channel.Name)
}
return nil
}
func (b *Birc) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EventMsgDelete {
return "", nil
}
b.Log.Debugf("=> Receiving %#v", msg)
// we can be in between reconnects #385
if !b.i.IsConnected() {
b.Log.Error("Not connected to server, dropping message")
}
// Execute a command
if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg)
}
// convert to specified charset
if err := b.handleCharset(&msg); err != nil {
return "", err
}
// handle files, return if we're done here
if ok := b.handleFiles(&msg); ok {
return "", nil
}
var msgLines []string
if b.GetBool("MessageSplit") {
msgLines = helper.GetSubLines(msg.Text, b.MessageLength)
} else {
msgLines = helper.GetSubLines(msg.Text, 0)
}
for i := range msgLines {
if len(b.Local) >= b.MessageQueue {
b.Log.Debugf("flooding, dropping message (queue at %d)", len(b.Local))
return "", nil
}
b.Local <- config.Message{
Text: msgLines[i],
Username: msg.Username,
Channel: msg.Channel,
Event: msg.Event,
}
}
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)
for msg := range b.Local {
<-throttle.C
username := msg.Username
if b.GetBool("Colornicks") {
checksum := crc32.ChecksumIEEE([]byte(msg.Username))
colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
}
if msg.Event == config.EventUserAction {
b.i.Cmd.Action(msg.Channel, username+msg.Text)
} else {
b.Log.Debugf("Sending to channel %s", msg.Channel)
b.i.Cmd.Message(msg.Channel, username+msg.Text)
}
}
}
// 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) port, err := strconv.Atoi(portstr)
if err != nil { if err != nil {
return nil, err return err
} }
// fix strict user handling of girc // fix strict user handling of girc
user := b.GetString("Nick") user := b.Config.Nick
for !girc.IsValidUser(user) { for !girc.IsValidUser(user) {
if len(user) == 1 { if len(user) == 1 {
user = "matterbridge" user = "matterbridge"
@ -240,51 +91,291 @@ func (b *Birc) getClient() (*girc.Client, error) {
i := girc.New(girc.Config{ i := girc.New(girc.Config{
Server: server, Server: server,
ServerPass: b.GetString("Password"), ServerPass: b.Config.Password,
Port: port, Port: port,
Nick: b.GetString("Nick"), Nick: b.Config.Nick,
User: user, User: user,
Name: b.GetString("Nick"), Name: b.Config.Nick,
SSL: b.GetBool("UseTLS"), SSL: b.Config.UseTLS,
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec TLSConfig: &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify, ServerName: server},
PingDelay: time.Minute, PingDelay: time.Minute,
}) })
return i, nil
if b.Config.UseSASL {
i.Config.SASL = &girc.SASLPlain{b.Config.NickServNick, b.Config.NickServPassword}
}
i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection)
i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
i.Handlers.Add("*", b.handleOther)
go func() {
for {
if err := i.Connect(); err != nil {
flog.Errorf("error: %s", err)
flog.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.EVENT_REJOIN_CHANNELS}
// set our correct nick on reconnect if necessary
b.Nick = event.Source.Name
})
} else {
return
}
}
}()
b.i = i
select {
case <-b.connected:
flog.Info("Connection succeeded")
case <-time.After(time.Second * 30):
return fmt.Errorf("connection timed out")
}
//i.Debug = false
i.Handlers.Clear("*")
go b.doSend()
return nil
}
func (b *Birc) Disconnect() error {
//b.i.Disconnect()
close(b.Local)
return nil
}
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
if channel.Options.Key != "" {
flog.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
b.i.Cmd.JoinKey(channel.Name, channel.Options.Key)
} else {
b.i.Cmd.Join(channel.Name)
}
return nil
}
func (b *Birc) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
flog.Debugf("Receiving %#v", msg)
if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg)
}
if b.Config.Charset != "" {
buf := new(bytes.Buffer)
w, err := charset.NewWriter(b.Config.Charset, buf)
if err != nil {
flog.Errorf("charset from utf-8 conversion failed: %s", err)
return "", err
}
fmt.Fprintf(w, msg.Text)
w.Close()
msg.Text = buf.String()
}
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text = fi.URL
}
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
}
return "", nil
}
}
// split long messages on messageLength, to avoid clipped messages #281
if b.Config.MessageSplit {
msg.Text = helper.SplitStringLength(msg.Text, b.Config.MessageLength)
}
for _, text := range strings.Split(msg.Text, "\n") {
input := []rune(text)
if len(text) > b.Config.MessageLength {
text = string(input[:b.Config.MessageLength]) + " <message clipped>"
}
if len(b.Local) < b.Config.MessageQueue {
if len(b.Local) == b.Config.MessageQueue-1 {
text = text + " <message clipped>"
}
b.Local <- config.Message{Text: text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
} else {
flog.Debugf("flooding, dropping message (queue at %d)", len(b.Local))
}
}
return "", nil
}
func (b *Birc) doSend() {
rate := time.Millisecond * time.Duration(b.Config.MessageDelay)
throttle := time.NewTicker(rate)
for msg := range b.Local {
<-throttle.C
if msg.Event == config.EVENT_USER_ACTION {
b.i.Cmd.Action(msg.Channel, msg.Username+msg.Text)
} else {
b.i.Cmd.Message(msg.Channel, msg.Username+msg.Text)
}
}
} }
func (b *Birc) endNames(client *girc.Client, event girc.Event) { func (b *Birc) endNames(client *girc.Client, event girc.Event) {
channel := event.Params[1] channel := event.Params[1]
sort.Strings(b.names[channel]) sort.Strings(b.names[channel])
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow() maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
continued := false
for len(b.names[channel]) > maxNamesPerPost { for len(b.names[channel]) > maxNamesPerPost {
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]), b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost], continued),
Channel: channel, Account: b.Account} Channel: channel, Account: b.Account}
b.names[channel] = b.names[channel][maxNamesPerPost:] b.names[channel] = b.names[channel][maxNamesPerPost:]
continued = true
} }
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel]), b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel], continued),
Channel: channel, Account: b.Account} Channel: channel, Account: b.Account}
b.names[channel] = nil b.names[channel] = nil
b.i.Handlers.Clear(girc.RPL_NAMREPLY) b.i.Handlers.Clear(girc.RPL_NAMREPLY)
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES) b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
} }
func (b *Birc) skipPrivMsg(event girc.Event) bool { func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
// Our nick can be changed flog.Debug("Registering callbacks")
b.Nick = b.i.GetNick() i := b.i
b.Nick = event.Params[0]
i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
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)
// we are now fully connected
b.connected <- struct{}{}
}
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
if len(event.Params) == 0 {
flog.Debugf("handleJoinPart: empty Params? %#v", event)
return
}
channel := event.Params[0]
if event.Command == "KICK" {
flog.Infof("Got kicked from %s by %s", channel, event.Source.Name)
time.Sleep(time.Duration(b.Config.RejoinDelay) * time.Second)
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
return
}
if event.Command == "QUIT" {
if event.Source.Name == b.Nick && strings.Contains(event.Trailing, "Ping timeout") {
flog.Infof("%s reconnecting ..", b.Account)
b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EVENT_FAILURE}
return
}
}
if event.Source.Name != b.Nick {
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
return
}
flog.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.Config.NickServNick {
b.i.Cmd.Message(b.Config.NickServNick, "IDENTIFY "+b.Config.NickServPassword)
} else {
b.handlePrivMsg(client, event)
}
}
func (b *Birc) handleOther(client *girc.Client, event girc.Event) {
switch event.Command {
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
return
}
flog.Debugf("%#v", event.String())
}
func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
if strings.EqualFold(b.Config.NickServNick, "Q@CServe.quakenet.org") {
flog.Debugf("Authenticating %s against %s", b.Config.NickServUsername, b.Config.NickServNick)
b.i.Cmd.Message(b.Config.NickServNick, "AUTH "+b.Config.NickServUsername+" "+b.Config.NickServPassword)
}
}
func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
b.Nick = b.i.GetNick()
// freenode doesn't send 001 as first reply // freenode doesn't send 001 as first reply
if event.Command == "NOTICE" { if event.Command == "NOTICE" {
return true return
} }
// don't forward queries to the bot // don't forward queries to the bot
if event.Params[0] == b.Nick { if event.Params[0] == b.Nick {
return true return
} }
// don't forward message from ourself // don't forward message from ourself
if event.Source.Name == b.Nick { if event.Source.Name == b.Nick {
return true return
} }
return false rmsg := config.Message{Username: event.Source.Name, Channel: event.Params[0], Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
flog.Debugf("handlePrivMsg() %s %s %#v", event.Source.Name, event.Trailing, event)
msg := ""
if event.IsAction() {
rmsg.Event = config.EVENT_USER_ACTION
}
msg += event.StripAction()
// strip IRC colors
re := regexp.MustCompile(`[[:cntrl:]](?:\d{1,2}(?:,\d{1,2})?)?`)
msg = re.ReplaceAllString(msg, "")
var r io.Reader
var err error
mycharset := b.Config.Charset
if mycharset == "" {
// detect what were sending so that we convert it to utf-8
detector := chardet.NewTextDetector()
result, err := detector.DetectBest([]byte(msg))
if err != nil {
flog.Infof("detection failed for msg: %#v", msg)
return
}
flog.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"
}
}
r, err = charset.NewReader(mycharset, strings.NewReader(msg))
if err != nil {
flog.Errorf("charset to utf-8 conversion failed: %s", err)
return
}
output, _ := ioutil.ReadAll(r)
msg = string(output)
flog.Debugf("Sending message from %s on %s to gateway", event.Params[0], b.Account)
rmsg.Text = msg
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 {
flog.Errorf("Invalid time stamp: %s", event.Params[3])
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
flog.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0))
} }
func (b *Birc) nicksPerRow() int { func (b *Birc) nicksPerRow() int {
@ -295,9 +386,9 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
channel := event.Params[2] channel := event.Params[2]
b.names[channel] = append( b.names[channel] = append(
b.names[channel], b.names[channel],
strings.Split(strings.TrimSpace(event.Last()), " ")...) strings.Split(strings.TrimSpace(event.Trailing), " ")...)
} }
func (b *Birc) formatnicks(nicks []string) string { func (b *Birc) formatnicks(nicks []string, continued bool) string {
return strings.Join(nicks, ", ") + " currently on IRC" return plainformatter(nicks, b.nicksPerRow())
} }

View File

@ -2,17 +2,15 @@ package bmatrix
import ( import (
"bytes" "bytes"
"fmt"
"html"
"mime" "mime"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
matrix "github.com/matterbridge/gomatrix" log "github.com/Sirupsen/logrus"
matrix "github.com/matrix-org/gomatrix"
) )
type Bmatrix struct { type Bmatrix struct {
@ -20,35 +18,42 @@ type Bmatrix struct {
UserID string UserID string
RoomMap map[string]string RoomMap map[string]string
sync.RWMutex sync.RWMutex
htmlTag *regexp.Regexp *config.BridgeConfig
*bridge.Config
} }
func New(cfg *bridge.Config) bridge.Bridger { var flog *log.Entry
b := &Bmatrix{Config: cfg} var protocol = "matrix"
b.htmlTag = regexp.MustCompile("</.*?>")
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bmatrix {
b := &Bmatrix{BridgeConfig: cfg}
b.RoomMap = make(map[string]string) b.RoomMap = make(map[string]string)
return b return b
} }
func (b *Bmatrix) Connect() error { func (b *Bmatrix) Connect() error {
var err error var err error
b.Log.Infof("Connecting %s", b.GetString("Server")) flog.Infof("Connecting %s", b.Config.Server)
b.mc, err = matrix.NewClient(b.GetString("Server"), "", "") b.mc, err = matrix.NewClient(b.Config.Server, "", "")
if err != nil { if err != nil {
flog.Debugf("%#v", err)
return err return err
} }
resp, err := b.mc.Login(&matrix.ReqLogin{ resp, err := b.mc.Login(&matrix.ReqLogin{
Type: "m.login.password", Type: "m.login.password",
User: b.GetString("Login"), User: b.Config.Login,
Password: b.GetString("Password"), Password: b.Config.Password,
}) })
if err != nil { if err != nil {
flog.Debugf("%#v", err)
return err return err
} }
b.mc.SetCredentials(resp.UserID, resp.AccessToken) b.mc.SetCredentials(resp.UserID, resp.AccessToken)
b.UserID = resp.UserID b.UserID = resp.UserID
b.Log.Info("Connection succeeded") flog.Info("Connection succeeded")
go b.handlematrix() go b.handlematrix()
return nil return nil
} }
@ -69,72 +74,58 @@ func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
} }
func (b *Bmatrix) Send(msg config.Message) (string, error) { func (b *Bmatrix) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
// ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
return "", nil
}
channel := b.getRoomID(msg.Channel) channel := b.getRoomID(msg.Channel)
b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel) flog.Debugf("Sending to channel %s", channel)
if msg.Event == config.EVENT_USER_ACTION {
// Make a action /me of the message b.mc.SendMessageEvent(channel, "m.room.message",
if msg.Event == config.EventUserAction { matrix.TextMessage{"m.emote", msg.Username + msg.Text})
m := matrix.TextMessage{ return "", nil
MsgType: "m.emote",
Body: msg.Username + msg.Text,
}
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
if err != nil {
return "", err
}
return resp.EventID, err
} }
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
if err != nil {
return "", err
}
return resp.EventID, err
}
// Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
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) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
return b.handleUploadFiles(&msg, channel) 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") {
flog.Debugf("uploading file: %s %s", fi.Name, mtype)
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
if err != nil {
flog.Errorf("file upload failed: %#v", err)
continue
}
if strings.Contains(mtype, "video") {
flog.Debugf("sendVideo %s", res.ContentURI)
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
if err != nil {
flog.Errorf("sendVideo failed: %#v", err)
}
}
if strings.Contains(mtype, "image") {
flog.Debugf("sendImage %s", res.ContentURI)
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
if err != nil {
flog.Errorf("sendImage failed: %#v", err)
}
}
flog.Debugf("result: %#v", res)
}
}
return "", nil
} }
} }
// Edit message if we have an ID b.mc.SendText(channel, msg.Username+msg.Text)
// matrix has no editing support return "", nil
// 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
}
return resp.EventID, err
} }
func (b *Bmatrix) getRoomID(channel string) string { func (b *Bmatrix) getRoomID(channel string) string {
@ -147,201 +138,64 @@ func (b *Bmatrix) getRoomID(channel string) string {
} }
return "" return ""
} }
func (b *Bmatrix) handlematrix() error {
func (b *Bmatrix) handlematrix() {
syncer := b.mc.Syncer.(*matrix.DefaultSyncer) syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
syncer.OnEventType("m.room.redaction", b.handleEvent) syncer.OnEventType("m.room.message", func(ev *matrix.Event) {
syncer.OnEventType("m.room.message", b.handleEvent) flog.Debugf("Received: %#v", ev)
if (ev.Content["msgtype"].(string) == "m.text" ||
ev.Content["msgtype"].(string) == "m.notice" ||
ev.Content["msgtype"].(string) == "m.emote" ||
ev.Content["msgtype"].(string) == "m.file" ||
ev.Content["msgtype"].(string) == "m.image" ||
ev.Content["msgtype"].(string) == "m.video") && ev.Sender != b.UserID {
b.RLock()
channel, ok := b.RoomMap[ev.RoomID]
b.RUnlock()
if !ok {
flog.Debugf("Unknown room %s", ev.RoomID)
return
}
username := ev.Sender[1:]
if b.Config.NoHomeServerSuffix {
re := regexp.MustCompile("(.*?):.*")
username = re.ReplaceAllString(username, `$1`)
}
rmsg := config.Message{Username: username, Text: ev.Content["body"].(string), Channel: channel, Account: b.Account, UserID: ev.Sender}
if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EVENT_USER_ACTION
}
if ev.Content["msgtype"].(string) == "m.image" ||
ev.Content["msgtype"].(string) == "m.video" ||
ev.Content["msgtype"].(string) == "m.file" {
flog.Debugf("ev: %#v", ev)
rmsg.Extra = make(map[string][]interface{})
url := ev.Content["url"].(string)
url = strings.Replace(url, "mxc://", b.Config.Server+"/_matrix/media/v1/download/", -1)
info := ev.Content["info"].(map[string]interface{})
size := info["size"].(float64)
name := ev.Content["body"].(string)
flog.Debugf("trying to download %#v with size %#v", name, size)
if size <= float64(b.General.MediaDownloadSize) {
data, err := helper.DownloadFile(url)
if err != nil {
flog.Errorf("download %s failed %#v", url, err)
} else {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
rmsg.Extra["file"] = append(rmsg.Extra["file"], config.FileInfo{Name: name, Data: data})
}
}
rmsg.Text = ""
}
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg
}
})
go func() { go func() {
for { for {
if err := b.mc.Sync(); err != nil { if err := b.mc.Sync(); err != nil {
b.Log.Println("Sync() returned ", err) flog.Println("Sync() returned ", err)
} }
} }
}() }()
}
func (b *Bmatrix) handleEvent(ev *matrix.Event) {
b.Log.Debugf("== Receiving event: %#v", ev)
if ev.Sender != b.UserID {
b.RLock()
channel, ok := b.RoomMap[ev.RoomID]
b.RUnlock()
if !ok {
b.Log.Debugf("Unknown room %s", ev.RoomID)
return
}
// TODO download avatar
// Create our message
rmsg := config.Message{Username: ev.Sender[1:], Channel: channel, Account: b.Account, UserID: ev.Sender, ID: ev.ID}
// Text must be a string
if rmsg.Text, ok = ev.Content["body"].(string); !ok {
b.Log.Errorf("Content[body] is not a string: %T\n%#v",
ev.Content["body"], ev.Content)
return
}
// Remove homeserver suffix if configured
if b.GetBool("NoHomeServerSuffix") {
re := regexp.MustCompile("(.*?):.*")
rmsg.Username = re.ReplaceAllString(rmsg.Username, `$1`)
}
// Delete event
if ev.Type == "m.room.redaction" {
rmsg.Event = config.EventMsgDelete
rmsg.ID = ev.Redacts
rmsg.Text = config.EventMsgDelete
b.Remote <- rmsg
return
}
// Do we have a /me action
if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EventUserAction
}
// Do we have attachments
if b.containsAttachment(ev.Content) {
err := b.handleDownloadFile(&rmsg, ev.Content)
if err != nil {
b.Log.Errorf("download failed: %#v", err)
}
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg
}
}
// handleDownloadFile handles file download
func (b *Bmatrix) handleDownloadFile(rmsg *config.Message, content map[string]interface{}) error {
var (
ok bool
url, name, msgtype, mtype string
info map[string]interface{}
size float64
)
rmsg.Extra = make(map[string][]interface{})
if url, ok = content["url"].(string); !ok {
return fmt.Errorf("url isn't a %T", url)
}
url = strings.Replace(url, "mxc://", b.GetString("Server")+"/_matrix/media/v1/download/", -1)
if info, ok = content["info"].(map[string]interface{}); !ok {
return fmt.Errorf("info isn't a %T", info)
}
if size, ok = info["size"].(float64); !ok {
return fmt.Errorf("size isn't a %T", size)
}
if name, ok = content["body"].(string); !ok {
return fmt.Errorf("name isn't a %T", name)
}
if msgtype, ok = content["msgtype"].(string); !ok {
return fmt.Errorf("msgtype isn't a %T", msgtype)
}
if mtype, ok = info["mimetype"].(string); !ok {
return fmt.Errorf("mtype isn't a %T", mtype)
}
// check if we have an image uploaded without extension
if !strings.Contains(name, ".") {
if msgtype == "m.image" {
mext, _ := mime.ExtensionsByType(mtype)
if len(mext) > 0 {
name += mext[0]
}
} else {
// just a default .png extension if we don't have mime info
name += ".png"
}
}
// check if the size is ok
err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
if err != nil {
return err
}
// actually download the file
data, err := helper.DownloadFile(url)
if err != nil {
return fmt.Errorf("download %s failed %#v", url, err)
}
// add the downloaded data to the message
helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General)
return nil return nil
} }
// handleUploadFiles handles native upload of files.
func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string, error) {
for _, f := range msg.Extra["file"] {
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
if content["msgtype"] == nil {
return false
}
// Only allow image,video or file msgtypes
if !(content["msgtype"].(string) == "m.image" ||
content["msgtype"].(string) == "m.video" ||
content["msgtype"].(string) == "m.file") {
return false
}
return true
}

View File

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

View File

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

View File

@ -3,29 +3,52 @@ package bmattermost
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient" "github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
"github.com/rs/xid" log "github.com/Sirupsen/logrus"
"strings"
) )
type Bmattermost struct { type MMhook struct {
mh *matterhook.Client mh *matterhook.Client
mc *matterclient.MMClient
uuid string
TeamID string
*bridge.Config
avatarMap map[string]string
} }
const mattermostPlugin = "mattermost.plugin" type MMapi struct {
mc *matterclient.MMClient
mmMap map[string]string
}
func New(cfg *bridge.Config) bridge.Bridger { type MMMessage struct {
b := &Bmattermost{Config: cfg, avatarMap: make(map[string]string)} Text string
b.uuid = xid.New().String() Channel string
Username string
UserID string
ID string
Event string
Extra map[string][]interface{}
}
type Bmattermost struct {
MMhook
MMapi
Config *config.Protocol
Remote chan config.Message
TeamId string
Account string
*config.BridgeConfig
}
var flog *log.Entry
var protocol = "mattermost"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bmattermost {
b := &Bmattermost{BridgeConfig: cfg}
b.mmMap = make(map[string]string)
return b return b
} }
@ -34,41 +57,71 @@ func (b *Bmattermost) Command(cmd string) string {
} }
func (b *Bmattermost) Connect() error { func (b *Bmattermost) Connect() error {
if b.Account == mattermostPlugin { if b.Config.WebhookBindAddress != "" {
return nil if b.Config.WebhookURL != "" {
} flog.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
if b.GetString("WebhookBindAddress") != "" { b.mh = matterhook.New(b.Config.WebhookURL,
if err := b.doConnectWebhookBind(); err != nil { matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
return err BindAddress: b.Config.WebhookBindAddress})
} else if b.Config.Token != "" {
flog.Info("Connecting using token (sending)")
err := b.apiLogin()
if err != nil {
return err
}
} else if b.Config.Login != "" {
flog.Info("Connecting using login/password (sending)")
err := b.apiLogin()
if err != nil {
return err
}
} else {
flog.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
BindAddress: b.Config.WebhookBindAddress})
} }
go b.handleMatter() go b.handleMatter()
return nil return nil
} }
switch { if b.Config.WebhookURL != "" {
case b.GetString("WebhookURL") != "": flog.Info("Connecting using webhookurl (sending)")
if err := b.doConnectWebhookURL(); err != nil { b.mh = matterhook.New(b.Config.WebhookURL,
return err matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
DisableServer: true})
if b.Config.Token != "" {
flog.Info("Connecting using token (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
} else if b.Config.Login != "" {
flog.Info("Connecting using login/password (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
} }
go b.handleMatter()
return nil return nil
case b.GetString("Token") != "": } else if b.Config.Token != "" {
b.Log.Info("Connecting using token (sending and receiving)") flog.Info("Connecting using token (sending and receiving)")
err := b.apiLogin() err := b.apiLogin()
if err != nil { if err != nil {
return err return err
} }
go b.handleMatter() go b.handleMatter()
case b.GetString("Login") != "": } else if b.Config.Login != "" {
b.Log.Info("Connecting using login/password (sending and receiving)") flog.Info("Connecting using login/password (sending and receiving)")
err := b.apiLogin() err := b.apiLogin()
if err != nil { if err != nil {
return err return err
} }
go b.handleMatter() go b.handleMatter()
} }
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" && if b.Config.WebhookBindAddress == "" && b.Config.WebhookURL == "" && b.Config.Login == "" && b.Config.Token == "" {
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 errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token/Login/Password/Server/Team configured")
} }
return nil return nil
} }
@ -78,12 +131,9 @@ func (b *Bmattermost) Disconnect() error {
} }
func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error { func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
if b.Account == mattermostPlugin {
return nil
}
// we can only join channels using the API // we can only join channels using the API
if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" { if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" {
id := b.mc.GetChannelId(channel.Name, b.TeamID) id := b.mc.GetChannelId(channel.Name, "")
if id == "" { if id == "" {
return fmt.Errorf("Could not find channel ID for channel %s", channel.Name) return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
} }
@ -93,56 +143,189 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
} }
func (b *Bmattermost) Send(msg config.Message) (string, error) { func (b *Bmattermost) Send(msg config.Message) (string, error) {
if b.Account == mattermostPlugin { flog.Debugf("Receiving %#v", msg)
return "", nil if msg.Event == config.EVENT_USER_ACTION {
}
b.Log.Debugf("=> Receiving %#v", msg)
// Make a action /me of the message
if msg.Event == config.EventUserAction {
msg.Text = "*" + msg.Text + "*" msg.Text = "*" + msg.Text + "*"
} }
nick := msg.Username
message := msg.Text
channel := msg.Channel
// map the file SHA to our user (caches the avatar) if b.Config.PrefixMessagesWithNick {
if msg.Event == config.EventAvatarDownload { message = nick + message
return b.cacheAvatar(&msg)
} }
if b.Config.WebhookURL != "" {
// Use webhook to send the message matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
if b.GetString("WebhookURL") != "" { matterMessage.IconURL = msg.Avatar
return b.sendWebhook(msg) matterMessage.Channel = channel
matterMessage.UserName = nick
matterMessage.Type = ""
matterMessage.Text = message
matterMessage.Text = message
matterMessage.Props = make(map[string]interface{})
matterMessage.Props["matterbridge"] = true
err := b.mh.Send(matterMessage)
if err != nil {
flog.Info(err)
return "", err
}
return "", nil
} }
if msg.Event == config.EVENT_MSG_DELETE {
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" { if msg.ID == "" {
return "", nil return "", nil
} }
return msg.ID, b.mc.DeleteMessage(msg.ID) return msg.ID, b.mc.DeleteMessage(msg.ID)
} }
// Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { if len(msg.Extra["file"]) > 0 {
if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil { var err error
b.Log.Errorf("PostMessage failed: %s", err) var res, id string
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
id, err = b.mc.UploadFile(*fi.Data, b.mc.GetChannelId(channel, ""), fi.Name)
if err != nil {
flog.Debugf("ERROR %#v", err)
return "", err
}
message = fi.Comment
if b.Config.PrefixMessagesWithNick {
message = nick + fi.Comment
}
res, err = b.mc.PostMessageWithFiles(b.mc.GetChannelId(channel, ""), message, []string{id})
}
return res, err
}
}
if msg.ID != "" {
return b.mc.EditMessage(msg.ID, message)
}
return b.mc.PostMessage(b.mc.GetChannelId(channel, ""), message)
}
func (b *Bmattermost) handleMatter() {
mchan := make(chan *MMMessage)
if b.Config.WebhookBindAddress != "" {
flog.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(mchan)
} else {
if b.Config.Token != "" {
flog.Debugf("Choosing token based receiving")
} else {
flog.Debugf("Choosing login/password based receiving")
}
go b.handleMatterClient(mchan)
}
for message := range mchan {
rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID, ID: message.ID, Event: message.Event, Extra: message.Extra}
text, ok := b.replaceAction(message.Text)
if ok {
rmsg.Event = config.EVENT_USER_ACTION
}
rmsg.Text = text
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
flog.Debugf("Message is %#v", rmsg)
b.Remote <- rmsg
}
}
func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
for message := range b.mc.MessageChan {
flog.Debugf("%#v", message.Raw.Data)
if message.Type == "system_join_leave" ||
message.Type == "system_join_channel" ||
message.Type == "system_leave_channel" {
flog.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.EVENT_JOIN_LEAVE}
continue
}
if (message.Raw.Event == "post_edited") && b.Config.EditDisable {
continue
}
m := &MMMessage{Extra: make(map[string][]interface{})}
props := message.Post.Props
if props != nil {
if _, ok := props["matterbridge"].(bool); ok {
flog.Debugf("sent by matterbridge, ignoring")
continue
}
if _, ok := props["override_username"].(string); ok {
message.Username = props["override_username"].(string)
}
if _, ok := props["attachments"].([]interface{}); ok {
m.Extra["attachments"] = props["attachments"].([]interface{})
} }
} }
if len(msg.Extra["file"]) > 0 { // do not post our own messages back to irc
return b.handleUploadFile(&msg) // only listen to message from our team
if (message.Raw.Event == "posted" || message.Raw.Event == "post_edited" || message.Raw.Event == "post_deleted") &&
b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId {
// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
if message.Post.HasReactions {
continue
}
flog.Debugf("Receiving from matterclient %#v", message)
m.UserID = message.UserID
m.Username = message.Username
m.Channel = message.Channel
m.Text = message.Text
m.ID = message.Post.Id
if message.Raw.Event == "post_edited" && !b.Config.EditDisable {
m.Text = message.Text + b.Config.EditSuffix
}
if message.Raw.Event == "post_deleted" {
m.Event = config.EVENT_MSG_DELETE
}
if len(message.Post.FileIds) > 0 {
for _, link := range b.mc.GetFileLinks(message.Post.FileIds) {
m.Text = m.Text + "\n" + link
}
}
mchan <- m
} }
} }
}
// Prepend nick if configured
if b.GetBool("PrefixMessagesWithNick") { func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
msg.Text = msg.Username + msg.Text for {
} message := b.mh.Receive()
flog.Debugf("Receiving from matterhook %#v", message)
// Edit message if we have an ID m := &MMMessage{}
if msg.ID != "" { m.UserID = message.UserID
return b.mc.EditMessage(msg.ID, msg.Text) m.Username = message.UserName
} m.Text = message.Text
m.Channel = message.ChannelName
// Post normal message mchan <- m
return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID) }
}
func (b *Bmattermost) apiLogin() error {
password := b.Config.Password
if b.Config.Token != "" {
password = "MMAUTHTOKEN=" + b.Config.Token
}
b.mc = matterclient.New(b.Config.Login, password,
b.Config.Team, b.Config.Server)
b.mc.SkipTLSVerify = b.Config.SkipTLSVerify
b.mc.NoTLS = b.Config.NoTLS
flog.Infof("Connecting %s (team: %s) on %s", b.Config.Login, b.Config.Team, b.Config.Server)
err := b.mc.Login()
if err != nil {
return err
}
flog.Info("Connection succeeded")
b.TeamId = b.mc.GetTeamId()
go b.mc.WsReceiver()
go b.mc.StatusLoop()
return nil
}
func (b *Bmattermost) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
return strings.Replace(text, "*", "", -1), true
}
return text, false
} }

View File

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

View File

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

View File

@ -1,46 +1,31 @@
package brocketchat package brocketchat
import ( import (
"errors"
"sync"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/hook/rockethook" "github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
lru "github.com/hashicorp/golang-lru" log "github.com/Sirupsen/logrus"
"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 { type MMhook struct {
mh *matterhook.Client mh *matterhook.Client
rh *rockethook.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 { type Brocketchat struct {
newCache, err := lru.New(100) MMhook
if err != nil { *config.BridgeConfig
cfg.Log.Fatalf("Could not create LRU cache for rocketchat bridge: %v", err) }
}
b := &Brocketchat{ var flog *log.Entry
Config: cfg, var protocol = "rocketchat"
messageChan: make(chan models.Message),
channelMap: make(map[string]string), func init() {
cache: newCache, flog = log.WithFields(log.Fields{"module": protocol})
} }
b.Log.Debugf("enabling rocketchat")
return b func New(cfg *config.BridgeConfig) *Brocketchat {
return &Brocketchat{BridgeConfig: cfg}
} }
func (b *Brocketchat) Command(cmd string) string { func (b *Brocketchat) Command(cmd string) string {
@ -48,118 +33,52 @@ func (b *Brocketchat) Command(cmd string) string {
} }
func (b *Brocketchat) Connect() error { func (b *Brocketchat) Connect() error {
if b.GetString("WebhookBindAddress") != "" { flog.Info("Connecting webhooks")
if err := b.doConnectWebhookBind(); err != nil { b.mh = matterhook.New(b.Config.WebhookURL,
return err matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
} DisableServer: true})
go b.handleRocket() b.rh = rockethook.New(b.Config.WebhookURL, rockethook.Config{BindAddress: b.Config.WebhookBindAddress})
return nil go b.handleRocketHook()
}
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 return nil
} }
func (b *Brocketchat) Disconnect() error { func (b *Brocketchat) Disconnect() error {
return nil return nil
} }
func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error { func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
if b.c == nil {
return nil
}
id, err := b.c.GetChannelId(channel.Name)
if err != nil {
return err
}
b.Lock()
b.channelMap[id] = channel.Name
b.Unlock()
mychannel := &models.Channel{ID: id, Name: 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 return nil
} }
func (b *Brocketchat) Send(msg config.Message) (string, error) { func (b *Brocketchat) Send(msg config.Message) (string, error) {
channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel} // ignore delete messages
if msg.Event == config.EVENT_MSG_DELETE {
// Delete message return "", nil
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
return msg.ID, b.c.DeleteMessage(&models.Message{ID: msg.ID})
} }
flog.Debugf("Receiving %#v", msg)
// Use webhook to send the message matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
if b.GetString("WebhookURL") != "" { matterMessage.Channel = msg.Channel
return "", b.sendWebhook(&msg) matterMessage.UserName = msg.Username
} matterMessage.Type = ""
matterMessage.Text = msg.Text
// Prepend nick if configured err := b.mh.Send(matterMessage)
if b.GetBool("PrefixMessagesWithNick") { if err != nil {
msg.Text = msg.Username + msg.Text flog.Info(err)
}
// 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) {
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)
}
}
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 "", err
} }
return rmsg.ID, err return "", nil
}
func (b *Brocketchat) handleRocketHook() {
for {
message := b.rh.Receive()
flog.Debugf("Receiving from rockethook %#v", message)
// do not loop
if message.UserName == b.Config.Nick {
continue
}
flog.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}
}
} }

View File

@ -1,379 +0,0 @@
package bslack
import (
"fmt"
"html"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/nlopes/slack"
)
func (b *Bslack) handleSlack() {
messages := make(chan *config.Message)
if b.GetString(incomingWebhookConfig) != "" {
b.Log.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(messages)
} else {
b.Log.Debugf("Choosing token based receiving")
go b.handleSlackClient(messages)
}
time.Sleep(time.Second)
b.Log.Debug("Start listening for Slack messages")
for message := range messages {
if message.Event != config.EventUserTyping {
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.getAvatar(message.UserID)
b.Log.Debugf("<= Message is %#v", message)
b.Remote <- *message
}
}
func (b *Bslack) handleSlackClient(messages chan *config.Message) {
for msg := range b.rtm.IncomingEvents {
if msg.Type != sUserTyping && msg.Type != sLatencyReport {
b.Log.Debugf("== Receiving event %#v", msg.Data)
}
switch ev := msg.Data.(type) {
case *slack.UserTypingEvent:
if !b.GetBool("ShowUserTyping") {
continue
}
rmsg, err := b.handleTypingEvent(ev)
if err != nil {
b.Log.Errorf("%#v", err)
continue
}
messages <- rmsg
case *slack.MessageEvent:
if b.skipMessageEvent(ev) {
b.Log.Debugf("Skipped message: %#v", ev)
continue
}
rmsg, err := b.handleMessageEvent(ev)
if err != nil {
b.Log.Errorf("%#v", err)
continue
}
messages <- rmsg
case *slack.OutgoingErrorEvent:
b.Log.Debugf("%#v", ev.Error())
case *slack.ChannelJoinedEvent:
// 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.channelsMutex.Lock()
b.channelsByID[ev.Channel.ID] = &ev.Channel
b.channelsByName[ev.Channel.Name] = &ev.Channel
b.channelsMutex.Unlock()
case *slack.ConnectedEvent:
b.si = ev.Info
b.populateChannels(true)
b.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.populateUser(ev.User)
case *slack.LatencyReport:
continue
default:
b.Log.Debugf("Unhandled incoming event: %T", ev)
}
}
}
func (b *Bslack) handleMatterHook(messages chan *config.Message) {
for {
message := b.mh.Receive()
b.Log.Debugf("receiving from matterhook (slack) %#v", message)
if message.UserName == "slackbot" {
continue
}
messages <- &config.Message{
Username: message.UserName,
Text: message.Text,
Channel: message.ChannelName,
}
}
}
// skipMessageEvent skips event that need to be skipped :-)
func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
switch ev.SubType {
case sChannelLeave, sChannelJoin:
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).
if ev.Username == sSlackBotUser ||
(b.rtm != nil && ev.Username == b.si.User.Name) ||
(len(ev.Attachments) > 0 && ev.Attachments[0].CallbackID == "matterbridge_"+b.uuid) {
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 len(ev.Files) > 0 {
return b.filesCached(ev.Files)
}
return false
}
func (b *Bslack) filesCached(files []slack.File) bool {
for i := range files {
if !b.fileCached(&files[i]) {
return false
}
}
return true
}
// handleMessageEvent handles the message events. Together with any called sub-methods,
// this method implements the following event processing pipeline:
//
// 1. Check if the message should be ignored.
// NOTE: This is not actually part of the method below but is done just before it
// is called via the 'skipMessageEvent()' method.
// 2. Populate the Matterbridge message that will be sent to the router based on the
// received event and logic that is common to all events that are not skipped.
// 3. Detect and handle any message that is "status" related (think join channel, etc.).
// This might result in an early exit from the pipeline and passing of the
// pre-populated message to the Matterbridge router.
// 4. Handle the specific case of messages that edit existing messages depending on
// configuration.
// 5. Handle any attachments of the received event.
// 6. Check that the Matterbridge message that we end up with after at the end of the
// pipeline is valid before sending it to the Matterbridge router.
func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, error) {
rmsg, err := b.populateReceivedMessage(ev)
if err != nil {
return nil, err
}
// Handle some message types early.
if b.handleStatusEvent(ev, rmsg) {
return rmsg, nil
}
b.handleAttachments(ev, rmsg)
// Verify that we have the right information and the message
// is well-formed before sending it out to the router.
if len(ev.Files) == 0 && (rmsg.Text == "" || rmsg.Username == "") {
if ev.BotID != "" {
// 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
}
func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
switch ev.SubType {
case sChannelJoined, sMemberJoined:
// There's no further processing needed on channel events
// so we return 'true'.
return true
case sChannelJoin, sChannelLeave:
rmsg.Username = sSystemUser
rmsg.Event = config.EventJoinLeave
case sChannelTopic, sChannelPurpose:
b.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
rmsg.ID = ev.DeletedTimestamp
// If a message is being deleted we do not need to process
// the event any further so we return 'true'.
return true
case sMeMessage:
rmsg.Event = config.EventUserAction
}
return false
}
func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) {
// File comments are set by the system (because there is no username given).
if ev.SubType == sFileComment {
rmsg.Username = sSystemUser
}
// See if we have some text in the attachments.
if rmsg.Text == "" {
for _, attach := range ev.Attachments {
if attach.Text != "" {
if attach.Title != "" {
rmsg.Text = attach.Title + "\n"
}
rmsg.Text += attach.Text
} else {
rmsg.Text = attach.Fallback
}
}
}
// Save the attachments, so that we can send them to other slack (compatible) bridges.
if len(ev.Attachments) > 0 {
rmsg.Extra[sSlackAttachment] = append(rmsg.Extra[sSlackAttachment], ev.Attachments)
}
// 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], 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)
if err != nil {
return nil, err
}
return &config.Message{
Channel: channelInfo.Name,
Account: b.Account,
Event: config.EventUserTyping,
}, nil
}
// handleDownloadFile handles file download
func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retry bool) error {
if b.fileCached(file) {
return nil
}
// Check that the file is neither too large nor blacklisted.
if err := helper.HandleDownloadSize(b.Log, rmsg, file.Name, int64(file.Size), b.General); err != nil {
b.Log.WithError(err).Infof("Skipping download of incoming file.")
return nil
}
// Actually download the file.
data, err := helper.DownloadFileAuth(file.URLPrivateDownload, "Bearer "+b.GetString(tokenConfig))
if err != nil {
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.
comment := rmsg.Text
rmsg.Text = ""
helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General)
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 := config.ChannelMembers{}
b.channelMembersMutex.RLock()
for channelID, members := range b.channelMembers {
for _, member := range members {
channelName := ""
userName := ""
userNick := ""
user := b.getUser(member)
if user != nil {
userName = user.Name
userNick = user.Profile.DisplayName
}
channel, _ := b.getChannelByID(channelID)
if channel != nil {
channelName = channel.Name
}
cMember := config.ChannelMember{
Username: userName,
Nick: userNick,
UserID: member,
ChannelID: channelID,
ChannelName: channelName,
}
cMembers = append(cMembers, cMember)
}
}
b.channelMembersMutex.RUnlock()
extra := make(map[string][]interface{})
extra[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.
//
// We consider that a file was cached if its ID was added in the last minute or
// it's name was registered in the last 10 seconds. This ensures that an
// identically named file but with different content will be uploaded correctly
// (the assumption is that such name collisions will not occur within the given
// timeframes).
func (b *Bslack) fileCached(file *slack.File) bool {
if ts, ok := b.cache.Get("file" + file.ID); ok && time.Since(ts.(time.Time)) < time.Minute {
return true
} else if ts, ok = b.cache.Get("filename" + file.Name); ok && time.Since(ts.(time.Time)) < 10*time.Second {
return true
}
return false
}

View File

@ -1,441 +0,0 @@
package bslack
import (
"context"
"fmt"
"regexp"
"strings"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/nlopes/slack"
)
func (b *Bslack) getUser(id string) *slack.User {
b.usersMutex.RLock()
user, ok := b.users[id]
b.usersMutex.RUnlock()
if ok {
return user
}
b.populateUser(id)
b.usersMutex.RLock()
defer b.usersMutex.RUnlock()
return b.users[id]
}
func (b *Bslack) getUsername(id string) string {
if user := b.getUser(id); user != nil {
if user.Profile.DisplayName != "" {
return user.Profile.DisplayName
}
return user.Name
}
b.Log.Warnf("Could not find user with ID '%s'", id)
return ""
}
func (b *Bslack) getAvatar(id string) string {
if user := b.getUser(id); user != nil {
return user.Profile.Image48
}
return ""
}
func (b *Bslack) getChannel(channel string) (*slack.Channel, error) {
if strings.HasPrefix(channel, "ID:") {
return b.getChannelByID(strings.TrimPrefix(channel, "ID:"))
}
return b.getChannelByName(channel)
}
func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) {
return b.getChannelBy(name, b.channelsByName)
}
func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) {
return b.getChannelBy(ID, b.channelsByID)
}
func (b *Bslack) getChannelBy(lookupKey string, lookupMap map[string]*slack.Channel) (*slack.Channel, error) {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
if channel, ok := lookupMap[lookupKey]; ok {
return channel, nil
}
return nil, fmt.Errorf("%s: channel %s not found", b.Account, lookupKey)
}
const minimumRefreshInterval = 10 * time.Second
func (b *Bslack) populateUser(userID string) {
b.usersMutex.RLock()
_, exists := b.users[userID]
b.usersMutex.RUnlock()
if exists {
// already in cache
return
}
user, err := b.sc.GetUserInfo(userID)
if err != nil {
b.Log.Debugf("GetUserInfo failed for %v: %v", userID, err)
return
}
b.usersMutex.Lock()
b.users[userID] = user
b.usersMutex.Unlock()
}
func (b *Bslack) populateUsers(wait bool) {
b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestUserRefresh) || b.refreshInProgress) {
b.Log.Debugf("Not refreshing user list as it was done less than %v ago.",
minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
for b.refreshInProgress {
b.refreshMutex.Unlock()
time.Sleep(time.Second)
b.refreshMutex.Lock()
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newUsers := map[string]*slack.User{}
pagination := b.sc.GetUsersPaginated(slack.GetUsersOptionLimit(200))
count := 0
for {
var err error
pagination, err = pagination.Next(context.Background())
time.Sleep(time.Second)
if err != nil {
if pagination.Done(err) {
break
}
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve users: %#v", err)
return
}
continue
}
for i := range pagination.Users {
newUsers[pagination.Users[i].ID] = &pagination.Users[i]
}
b.Log.Debugf("getting %d users", len(pagination.Users))
count++
// more > 2000 users, slack will complain and ratelimit. break
if count > 10 {
b.Log.Info("Large slack detected > 2000 users, skipping loading complete userlist.")
break
}
}
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
b.users = newUsers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestUserRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
func (b *Bslack) populateChannels(wait bool) {
b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestChannelRefresh) || b.refreshInProgress) {
b.Log.Debugf("Not refreshing channel list as it was done less than %v seconds ago.",
minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
for b.refreshInProgress {
b.refreshMutex.Unlock()
time.Sleep(time.Second)
b.refreshMutex.Lock()
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newChannelsByID := map[string]*slack.Channel{}
newChannelsByName := map[string]*slack.Channel{}
newChannelMembers := make(map[string][]string)
// We only retrieve public and private channels, not IMs
// and MPIMs as those do not have a channel name.
queryParams := &slack.GetConversationsParameters{
ExcludeArchived: "true",
Types: []string{"public_channel,private_channel"},
}
for {
channels, nextCursor, err := b.sc.GetConversations(queryParams)
if err != nil {
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve channels: %#v", err)
return
}
continue
}
for i := range channels {
newChannelsByID[channels[i].ID] = &channels[i]
newChannelsByName[channels[i].Name] = &channels[i]
// also find all the members in every channel
// comment for now, issues on big slacks
/*
members, err := b.getUsersInConversation(channels[i].ID)
if err != nil {
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve channel members: %#v", err)
return
}
continue
}
newChannelMembers[channels[i].ID] = members
*/
}
if nextCursor == "" {
break
}
queryParams.Cursor = nextCursor
}
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelsByID = newChannelsByID
b.channelsByName = newChannelsByName
b.channelMembersMutex.Lock()
defer b.channelMembersMutex.Unlock()
b.channelMembers = newChannelMembers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestChannelRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
// 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)
if err != nil {
return nil, err
}
rmsg := &config.Message{
Text: ev.Text,
Channel: channel.Name,
Account: b.Account,
ID: ev.Timestamp,
Extra: make(map[string][]interface{}),
ParentID: ev.ThreadTimestamp,
Protocol: b.Protocol,
}
if b.useChannelID {
rmsg.Channel = "ID:" + channel.ID
}
// Handle 'edit' messages.
if ev.SubMessage != nil && !b.GetBool(editDisableConfig) {
rmsg.ID = ev.SubMessage.Timestamp
if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp {
b.Log.Debugf("SubMessage %#v", ev.SubMessage)
rmsg.Text = ev.SubMessage.Text + b.GetString(editSuffixConfig)
}
}
// 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
}
return rmsg, err
}
func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *config.Message) error {
if ev.SubType == sMessageDeleted || ev.SubType == sFileComment {
return nil
}
// First, deal with bot-originating messages but only do so when not using webhooks: we
// would not be able to distinguish which bot would be sending them.
if err := b.populateMessageWithBotInfo(ev, rmsg); err != nil {
return err
}
// Second, deal with "real" users if we have the necessary information.
var userID string
switch {
case ev.User != "":
userID = ev.User
case ev.SubMessage != nil && ev.SubMessage.User != "":
userID = ev.SubMessage.User
default:
return nil
}
user := b.getUser(userID)
if user == nil {
return fmt.Errorf("could not find information for user with id %s", ev.User)
}
rmsg.UserID = user.ID
rmsg.Username = user.Name
if user.Profile.DisplayName != "" {
rmsg.Username = user.Profile.DisplayName
}
return nil
}
func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config.Message) error {
if ev.BotID == "" || b.GetString(outgoingWebhookConfig) != "" {
return nil
}
var err error
var bot *slack.Bot
for {
bot, err = b.rtm.GetBotInfo(ev.BotID)
if err == nil {
break
}
if err = b.handleRateLimit(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 != "" {
rmsg.Username = bot.Name
if ev.Username != "" {
rmsg.Username = ev.Username
}
rmsg.UserID = bot.ID
}
return nil
}
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(`<(.*?)(\|.*?)?>`)
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 != "" {
return "@" + username
}
return match
}
return mentionRE.ReplaceAllStringFunc(text, replaceFunc)
}
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (b *Bslack) replaceChannel(text string) string {
for _, r := range channelRE.FindAllStringSubmatch(text, -1) {
text = strings.Replace(text, r[0], "#"+r[1], 1)
}
return text
}
// @see https://api.slack.com/docs/message-formatting#variables
func (b *Bslack) replaceVariable(text string) string {
for _, r := range variableRE.FindAllStringSubmatch(text, -1) {
if r[2] != "" {
text = strings.Replace(text, r[0], "@"+r[2], 1)
} else {
text = strings.Replace(text, r[0], "@"+r[1], 1)
}
}
return text
}
// @see https://api.slack.com/docs/message-formatting#linking_to_urls
func (b *Bslack) replaceURL(text string) string {
for _, r := range urlRE.FindAllStringSubmatch(text, -1) {
if len(strings.TrimSpace(r[2])) == 1 { // A display text separator was found, but the text was blank
text = strings.Replace(text, r[0], "", 1)
} else {
text = strings.Replace(text, r[0], r[1], 1)
}
}
return text
}
func (b *Bslack) replaceCodeFence(text string) string {
return codeFenceRE.ReplaceAllString(text, "```")
}
func (b *Bslack) handleRateLimit(err error) error {
rateLimit, ok := err.(*slack.RateLimitedError)
if !ok {
return err
}
b.Log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
time.Sleep(rateLimit.RetryAfter)
return nil
}
// getUsersInConversation returns an array of userIDs that are members of channelID
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 = b.handleRateLimit(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
}

View File

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

View File

@ -1,74 +0,0 @@
package bslack
import (
"errors"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/matterhook"
"github.com/nlopes/slack"
)
type BLegacy struct {
*Bslack
}
func NewLegacy(cfg *bridge.Config) bridge.Bridger {
return &BLegacy{Bslack: newBridge(cfg)}
}
func (b *BLegacy) Connect() error {
b.RLock()
defer b.RUnlock()
if b.GetString(incomingWebhookConfig) != "" {
switch {
case b.GetString(outgoingWebhookConfig) != "":
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
InsecureSkipVerify: b.GetBool(skipTLSConfig),
BindAddress: b.GetString(incomingWebhookConfig),
})
case b.GetString(tokenConfig) != "":
b.Log.Info("Connecting using token (sending)")
b.sc = slack.New(b.GetString(tokenConfig))
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
InsecureSkipVerify: b.GetBool(skipTLSConfig),
BindAddress: b.GetString(incomingWebhookConfig),
})
default:
b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
InsecureSkipVerify: b.GetBool(skipTLSConfig),
BindAddress: b.GetString(incomingWebhookConfig),
})
}
go b.handleSlack()
return nil
}
if b.GetString(outgoingWebhookConfig) != "" {
b.Log.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
InsecureSkipVerify: b.GetBool(skipTLSConfig),
DisableServer: true,
})
if b.GetString(tokenConfig) != "" {
b.Log.Info("Connecting using token (receiving)")
b.sc = slack.New(b.GetString(tokenConfig))
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.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
}
if b.GetString(incomingWebhookConfig) == "" && b.GetString(outgoingWebhookConfig) == "" && b.GetString(tokenConfig) == "" {
return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token configured")
}
return nil
}

View File

@ -4,106 +4,46 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
"github.com/hashicorp/golang-lru" log "github.com/Sirupsen/logrus"
"github.com/nlopes/slack" "github.com/matterbridge/slack"
"github.com/rs/xid" "html"
"io"
"net/http"
"regexp"
"strings"
"time"
) )
type MMMessage struct {
Text string
Channel string
Username string
UserID string
Raw *slack.MessageEvent
}
type Bslack struct { type Bslack struct {
sync.RWMutex mh *matterhook.Client
*bridge.Config sc *slack.Client
rtm *slack.RTM
mh *matterhook.Client Plus bool
sc *slack.Client Users []slack.User
rtm *slack.RTM si *slack.Info
si *slack.Info channels []slack.Channel
*config.BridgeConfig
cache *lru.Cache
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
channelMembers map[string][]string
channelMembersMutex sync.RWMutex
refreshInProgress bool
earliestChannelRefresh time.Time
earliestUserRefresh time.Time
refreshMutex sync.Mutex
} }
const ( var flog *log.Entry
sChannelJoin = "channel_join" var protocol = "slack"
sChannelLeave = "channel_leave"
sChannelJoined = "channel_joined"
sMemberJoined = "member_joined_channel"
sMessageChanged = "message_changed"
sMessageDeleted = "message_deleted"
sSlackAttachment = "slack_attachment"
sPinnedItem = "pinned_item"
sUnpinnedItem = "unpinned_item"
sChannelTopic = "channel_topic"
sChannelPurpose = "channel_purpose"
sFileComment = "file_comment"
sMeMessage = "me_message"
sUserTyping = "user_typing"
sLatencyReport = "latency_report"
sSystemUser = "system"
sSlackBotUser = "slackbot"
tokenConfig = "Token" func init() {
incomingWebhookConfig = "WebhookBindAddress" flog = log.WithFields(log.Fields{"module": protocol})
outgoingWebhookConfig = "WebhookURL"
skipTLSConfig = "SkipTLSVerify"
useNickPrefixConfig = "PrefixMessagesWithNick"
editDisableConfig = "EditDisable"
editSuffixConfig = "EditSuffix"
iconURLConfig = "iconurl"
noSendJoinConfig = "nosendjoinpart"
)
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.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)
} }
func newBridge(cfg *bridge.Config) *Bslack { func New(cfg *config.BridgeConfig) *Bslack {
newCache, err := lru.New(5000) return &Bslack{BridgeConfig: cfg}
if err != nil {
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(),
}
return b
} }
func (b *Bslack) Command(cmd string) string { func (b *Bslack) Command(cmd string) string {
@ -111,431 +51,460 @@ func (b *Bslack) Command(cmd string) string {
} }
func (b *Bslack) Connect() error { func (b *Bslack) Connect() error {
b.RLock() if b.Config.WebhookBindAddress != "" {
defer b.RUnlock() if b.Config.WebhookURL != "" {
flog.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
if b.GetString(incomingWebhookConfig) == "" && b.GetString(outgoingWebhookConfig) == "" && b.GetString(tokenConfig) == "" { b.mh = matterhook.New(b.Config.WebhookURL,
return errors.New("no connection method found: WebhookBindAddress, WebhookURL or Token need to be configured") matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
} BindAddress: b.Config.WebhookBindAddress})
} else if b.Config.Token != "" {
// If we have a token we use the Slack websocket-based RTM for both sending and receiving. flog.Info("Connecting using token (sending)")
if token := b.GetString(tokenConfig); token != "" { b.sc = slack.New(b.Config.Token)
b.Log.Info("Connecting using token") b.rtm = b.sc.NewRTM()
b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug"))) go b.rtm.ManageConnection()
b.rtm = b.sc.NewRTM() flog.Info("Connecting using webhookbindaddress (receiving)")
go b.rtm.ManageConnection() b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
BindAddress: b.Config.WebhookBindAddress})
} else {
flog.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.Config.WebhookURL,
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
BindAddress: b.Config.WebhookBindAddress})
}
go b.handleSlack() go b.handleSlack()
return nil return nil
} }
if b.Config.WebhookURL != "" {
// In absence of a token we fall back to incoming and outgoing Webhooks. flog.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New( b.mh = matterhook.New(b.Config.WebhookURL,
"", matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
matterhook.Config{ DisableServer: true})
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), if b.Config.Token != "" {
DisableServer: true, flog.Info("Connecting using token (receiving)")
}, b.sc = slack.New(b.Config.Token)
) b.rtm = b.sc.NewRTM()
if b.GetString(outgoingWebhookConfig) != "" { go b.rtm.ManageConnection()
b.Log.Info("Using specified webhook for outgoing messages.") go b.handleSlack()
b.mh.Url = b.GetString(outgoingWebhookConfig) }
} } else if b.Config.Token != "" {
if b.GetString(incomingWebhookConfig) != "" { flog.Info("Connecting using token (sending and receiving)")
b.Log.Info("Setting up local webhook for incoming messages.") b.sc = slack.New(b.Config.Token)
b.mh.BindAddress = b.GetString(incomingWebhookConfig) b.rtm = b.sc.NewRTM()
b.mh.DisableServer = false go b.rtm.ManageConnection()
go b.handleSlack() go b.handleSlack()
} }
if b.Config.WebhookBindAddress == "" && b.Config.WebhookURL == "" && b.Config.Token == "" {
return errors.New("No connection method found. See that you have WebhookBindAddress, WebhookURL or Token configured.")
}
return nil return nil
} }
func (b *Bslack) Disconnect() error { func (b *Bslack) Disconnect() error {
return b.rtm.Disconnect() return nil
} }
// JoinChannel only acts as a verification method that checks whether Matterbridge's
// Slack integration is already member of the channel. This is because Slack does not
// allow apps or bots to join channels themselves and they need to be invited
// manually by a user.
func (b *Bslack) JoinChannel(channel config.ChannelInfo) error { func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
// We can only join a channel through the Slack API. // we can only join channels using the API
if b.sc == nil { if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" {
return nil if strings.HasPrefix(b.Config.Token, "xoxb") {
} // TODO check if bot has already joined channel
return nil
b.populateChannels(false) }
_, err := b.sc.JoinChannel(channel.Name)
channelInfo, err := b.getChannel(channel.Name) if err != nil {
if err != nil { if err.Error() != "name_taken" {
return fmt.Errorf("could not join channel: %#v", err) return err
} }
}
if strings.HasPrefix(channel.Name, "ID:") {
b.useChannelID = true
channel.Name = channelInfo.Name
}
if !channelInfo.IsMember {
return fmt.Errorf("slack integration that matterbridge is using is not member of channel '%s', please add it manually", channelInfo.Name)
} }
return nil return nil
} }
func (b *Bslack) Reload(cfg *bridge.Config) (string, error) {
return "", nil
}
func (b *Bslack) Send(msg config.Message) (string, error) { func (b *Bslack) Send(msg config.Message) (string, error) {
// Too noisy to log like other events flog.Debugf("Receiving %#v", msg)
if msg.Event != config.EventUserTyping { if msg.Event == config.EVENT_USER_ACTION {
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 + "_" msg.Text = "_" + msg.Text + "_"
} }
nick := msg.Username
// Use webhook to send the message message := msg.Text
if b.GetString(outgoingWebhookConfig) != "" { channel := msg.Channel
return "", b.sendWebhook(msg) if b.Config.PrefixMessagesWithNick {
message = nick + " " + message
} }
return b.sendRTM(msg) if b.Config.WebhookURL != "" {
} matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
matterMessage.Channel = channel
// sendWebhook uses the configured WebhookURL to send the message matterMessage.UserName = nick
func (b *Bslack) sendWebhook(msg config.Message) error { matterMessage.Type = ""
// Skip events. matterMessage.Text = message
if msg.Event != "" { err := b.mh.Send(matterMessage)
return nil if err != nil {
} flog.Info(err)
if b.GetBool(useNickPrefixConfig) {
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(iconURLConfig))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Channel: msg.Channel,
UserName: rmsg.Username,
Text: rmsg.Text,
}
if err := b.mh.Send(matterMessage); err != nil {
b.Log.Errorf("Failed to send message: %v", err)
}
}
// Webhook doesn't support file uploads, so we add the URL manually.
for _, f := range msg.Extra["file"] {
fi, ok := f.(config.FileInfo)
if !ok {
b.Log.Errorf("Received a file with unexpected content: %#v", f)
continue
}
if fi.URL != "" {
msg.Text += " " + fi.URL
}
}
}
// If we have native slack_attachments add them.
var attachs []slack.Attachment
for _, attach := range msg.Extra[sSlackAttachment] {
attachs = append(attachs, attach.([]slack.Attachment)...)
}
iconURL := config.GetIconURL(&msg, b.GetString(iconURLConfig))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Attachments: attachs,
Channel: msg.Channel,
UserName: msg.Username,
Text: msg.Text,
}
if msg.Avatar != "" {
matterMessage.IconURL = msg.Avatar
}
if err := b.mh.Send(matterMessage); err != nil {
b.Log.Errorf("Failed to send message via webhook: %#v", err)
return err
}
return nil
}
func (b *Bslack) sendRTM(msg config.Message) (string, error) {
// Handle channelmember messages.
if handled := b.handleGetChannelMembers(&msg); handled {
return "", nil
}
channelInfo, err := b.getChannel(msg.Channel)
if err != nil {
return "", fmt.Errorf("could not send message: %v", err)
}
if msg.Event == config.EventUserTyping {
if b.GetBool("ShowUserTyping") {
b.rtm.SendMessage(b.rtm.NewTypingMessage(channelInfo.ID))
}
return "", nil
}
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
}
// Prepend nickname if configured.
if b.GetBool(useNickPrefixConfig) {
msg.Text = msg.Username + msg.Text
}
// Handle message edits.
if handled, err = b.editMessage(&msg, channelInfo); handled {
return msg.ID, err
}
// Upload a file if it exists.
if msg.Extra != nil {
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)
}
}
// Upload files if necessary (from Slack, Telegram or Mattermost).
b.uploadFile(&msg, channelInfo.ID)
}
// Post message.
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 = b.handleRateLimit(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) {
if msg.Event != config.EventMsgDelete {
return false, nil
}
// Some protocols echo deletes, but with an empty ID.
if msg.ID == "" {
return true, nil
}
for {
_, _, err := b.rtm.DeleteMessage(channelInfo.ID, msg.ID)
if err == nil {
return true, nil
}
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Failed to delete user message from Slack: %#v", err)
return true, err
}
}
}
func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
if msg.ID == "" {
return false, nil
}
messageOptions := b.prepareMessageOptions(msg)
for {
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 {
b.Log.Errorf("Failed to edit user message on Slack: %#v", err)
return true, err
}
}
}
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))
for {
_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
if err == nil {
return id, nil
}
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Failed to sent user message to Slack: %#v", err)
return "", err return "", err
} }
return "", nil
} }
schannel, err := b.getChannelByName(channel)
if err != nil {
return "", err
}
np := slack.NewPostMessageParameters()
if b.Config.PrefixMessagesWithNick {
np.AsUser = true
}
np.Username = nick
np.IconURL = config.GetIconURL(&msg, &b.Config)
if msg.Avatar != "" {
np.IconURL = msg.Avatar
}
np.Attachments = append(np.Attachments, slack.Attachment{CallbackID: "matterbridge"})
np.Attachments = append(np.Attachments, b.createAttach(msg.Extra)...)
// replace mentions
np.LinkNames = 1
if msg.Event == config.EVENT_MSG_DELETE {
// some protocols echo deletes, but with empty ID
if msg.ID == "" {
return "", nil
}
// we get a "slack <ID>", split it
ts := strings.Fields(msg.ID)
b.sc.DeleteMessage(schannel.ID, ts[1])
return "", nil
}
// if we have no ID it means we're creating a new message, not updating an existing one
if msg.ID != "" {
ts := strings.Fields(msg.ID)
b.sc.UpdateMessage(schannel.ID, ts[1], message)
return "", nil
}
if msg.Extra != nil {
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
var err error
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
_, err = b.sc.UploadFile(slack.FileUploadParameters{
Reader: bytes.NewReader(*fi.Data),
Filename: fi.Name,
Channels: []string{schannel.ID},
InitialComment: fi.Comment,
})
if err != nil {
flog.Errorf("uploadfile %#v", err)
}
}
}
}
_, id, err := b.sc.PostMessage(schannel.ID, message, np)
if err != nil {
return "", err
}
return "slack " + id, nil
} }
// uploadFile handles native upload of files func (b *Bslack) getAvatar(user string) string {
func (b *Bslack) uploadFile(msg *config.Message, channelID string) { var avatar string
for _, f := range msg.Extra["file"] { if b.Users != nil {
fi, ok := f.(config.FileInfo) for _, u := range b.Users {
if !ok { if user == u.Name {
b.Log.Errorf("Received a file with unexpected content: %#v", f) return u.Profile.Image48
}
}
}
return avatar
}
func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) {
if b.channels == nil {
return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, name)
}
for _, channel := range b.channels {
if channel.Name == name {
return &channel, nil
}
}
return nil, fmt.Errorf("%s: channel %s not found", b.Account, name)
}
func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) {
if b.channels == nil {
return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, ID)
}
for _, channel := range b.channels {
if channel.ID == ID {
return &channel, nil
}
}
return nil, fmt.Errorf("%s: channel %s not found", b.Account, ID)
}
func (b *Bslack) handleSlack() {
mchan := make(chan *MMMessage)
if b.Config.WebhookBindAddress != "" {
flog.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(mchan)
} else {
flog.Debugf("Choosing token based receiving")
go b.handleSlackClient(mchan)
}
time.Sleep(time.Second)
flog.Debug("Start listening for Slack messages")
for message := range mchan {
// do not send messages from ourself
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" && message.Username == b.si.User.Name {
continue continue
} }
if msg.Text == fi.Comment { if (message.Text == "" || message.Username == "") && message.Raw.SubType != "message_deleted" {
msg.Text = "" continue
} }
// Because the result of the UploadFile is slower than the MessageEvent from slack text := message.Text
// we can't match on the file ID yet, so we have to match on the filename too. text = b.replaceURL(text)
ts := time.Now() text = html.UnescapeString(text)
b.Log.Debugf("Adding file %s to cache at %s with timestamp", fi.Name, ts.String()) flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
b.cache.Add("filename"+fi.Name, ts) msg := config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username), UserID: message.UserID, ID: "slack " + message.Raw.Timestamp, Extra: make(map[string][]interface{})}
initialComment := fmt.Sprintf("File from %s", msg.Username) if message.Raw.SubType == "me_message" {
if fi.Comment != "" { msg.Event = config.EVENT_USER_ACTION
initialComment += fmt.Sprintf("with comment: %s", fi.Comment)
} }
res, err := b.sc.UploadFile(slack.FileUploadParameters{ if message.Raw.SubType == "channel_leave" || message.Raw.SubType == "channel_join" {
Reader: bytes.NewReader(*fi.Data), msg.Username = "system"
Filename: fi.Name, msg.Event = config.EVENT_JOIN_LEAVE
Channels: []string{channelID},
InitialComment: initialComment,
ThreadTimestamp: msg.ParentID,
})
if err != nil {
b.Log.Errorf("uploadfile %#v", err)
return
} }
if res.ID != "" { // edited messages have a submessage, use this timestamp
b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String()) if message.Raw.SubMessage != nil {
b.cache.Add("file"+res.ID, ts) msg.ID = "slack " + message.Raw.SubMessage.Timestamp
}
if message.Raw.SubType == "message_deleted" {
msg.Text = config.EVENT_MSG_DELETE
msg.Event = config.EVENT_MSG_DELETE
msg.ID = "slack " + message.Raw.DeletedTimestamp
}
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
if message.Raw.File != nil {
// limit to 1MB for now
if message.Raw.File.Size <= b.General.MediaDownloadSize {
comment := ""
data, err := b.downloadFile(message.Raw.File.URLPrivateDownload)
if err != nil {
flog.Errorf("download %s failed %#v", message.Raw.File.URLPrivateDownload, err)
} else {
results := regexp.MustCompile(`.*?commented: (.*)`).FindAllStringSubmatch(msg.Text, -1)
if len(results) > 0 {
comment = results[0][1]
}
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data, Comment: comment})
}
}
}
flog.Debugf("Message is %#v", msg)
b.Remote <- msg
}
}
func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
for msg := range b.rtm.IncomingEvents {
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
flog.Debugf("Receiving from slackclient %#v", ev)
if len(ev.Attachments) > 0 {
// skip messages we made ourselves
if ev.Attachments[0].CallbackID == "matterbridge" {
continue
}
}
if !b.Config.EditDisable && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp {
flog.Debugf("SubMessage %#v", ev.SubMessage)
ev.User = ev.SubMessage.User
ev.Text = ev.SubMessage.Text + b.Config.EditSuffix
// it seems ev.SubMessage.Edited == nil when slack unfurls
// do not forward these messages #266
if ev.SubMessage.Edited == nil {
continue
}
}
// use our own func because rtm.GetChannelInfo doesn't work for private channels
channel, err := b.getChannelByID(ev.Channel)
if err != nil {
continue
}
m := &MMMessage{}
if ev.BotID == "" && ev.SubType != "message_deleted" {
user, err := b.rtm.GetUserInfo(ev.User)
if err != nil {
continue
}
m.UserID = user.ID
m.Username = user.Name
if user.Profile.DisplayName != "" {
m.Username = user.Profile.DisplayName
}
}
m.Channel = channel.Name
m.Text = ev.Text
if m.Text == "" {
for _, attach := range ev.Attachments {
if attach.Text != "" {
m.Text = attach.Text
} else {
m.Text = attach.Fallback
}
}
}
m.Raw = ev
m.Text = b.replaceMention(m.Text)
m.Text = b.replaceVariable(m.Text)
m.Text = b.replaceChannel(m.Text)
// when using webhookURL we can't check if it's our webhook or not for now
if ev.BotID != "" && b.Config.WebhookURL == "" {
bot, err := b.rtm.GetBotInfo(ev.BotID)
if err != nil {
continue
}
if bot.Name != "" {
m.Username = bot.Name
if ev.Username != "" {
m.Username = ev.Username
}
m.UserID = bot.ID
}
}
mchan <- m
case *slack.OutgoingErrorEvent:
flog.Debugf("%#v", ev.Error())
case *slack.ChannelJoinedEvent:
b.Users, _ = b.sc.GetUsers()
case *slack.ConnectedEvent:
b.channels = ev.Info.Channels
b.si = ev.Info
b.Users, _ = b.sc.GetUsers()
// add private channels
groups, _ := b.sc.GetGroups(true)
for _, g := range groups {
channel := new(slack.Channel)
channel.ID = g.ID
channel.Name = g.Name
b.channels = append(b.channels, *channel)
}
case *slack.InvalidAuthEvent:
flog.Fatalf("Invalid Token %#v", ev)
default:
} }
} }
} }
func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption { func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
params := slack.NewPostMessageParameters() for {
if b.GetBool(useNickPrefixConfig) { message := b.mh.Receive()
params.AsUser = true flog.Debugf("receiving from matterhook (slack) %#v", message)
} m := &MMMessage{}
params.Username = msg.Username m.Username = message.UserName
params.LinkNames = 1 // replace mentions m.Text = message.Text
params.IconURL = config.GetIconURL(msg, b.GetString(iconURLConfig)) m.Text = b.replaceMention(m.Text)
params.ThreadTimestamp = msg.ParentID m.Text = b.replaceVariable(m.Text)
if msg.Avatar != "" { m.Text = b.replaceChannel(m.Text)
params.IconURL = msg.Avatar m.Channel = message.ChannelName
} if m.Username == "slackbot" {
continue
var attachments []slack.Attachment
// add a callback ID so we can see we created it
attachments = append(attachments, slack.Attachment{CallbackID: "matterbridge_" + b.uuid})
// add file attachments
attachments = append(attachments, b.createAttach(msg.Extra)...)
// add slack attachments (from another slack bridge)
if msg.Extra != nil {
for _, attach := range msg.Extra[sSlackAttachment] {
attachments = append(attachments, attach.([]slack.Attachment)...)
} }
mchan <- m
} }
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 { func (b *Bslack) userName(id string) string {
var attachements []slack.Attachment for _, u := range b.Users {
for _, v := range extra["attachments"] { if u.ID == id {
entry := v.(map[string]interface{}) if u.Profile.DisplayName != "" {
s := slack.Attachment{ return u.Profile.DisplayName
Fallback: extractStringField(entry, "fallback"), }
Color: extractStringField(entry, "color"), return u.Name
Pretext: extractStringField(entry, "pretext"),
AuthorName: extractStringField(entry, "author_name"),
AuthorLink: extractStringField(entry, "author_link"),
AuthorIcon: extractStringField(entry, "author_icon"),
Title: extractStringField(entry, "title"),
TitleLink: extractStringField(entry, "title_link"),
Text: extractStringField(entry, "text"),
ImageURL: extractStringField(entry, "image_url"),
ThumbURL: extractStringField(entry, "thumb_url"),
Footer: extractStringField(entry, "footer"),
FooterIcon: extractStringField(entry, "footer_icon"),
}
attachements = append(attachements, s)
}
return attachements
}
func extractStringField(data map[string]interface{}, field string) string {
if rawValue, found := data[field]; found {
if value, ok := rawValue.(string); ok {
return value
} }
} }
return "" return ""
} }
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (b *Bslack) replaceMention(text string) string {
results := regexp.MustCompile(`<@([a-zA-z0-9]+)>`).FindAllStringSubmatch(text, -1)
for _, r := range results {
text = strings.Replace(text, "<@"+r[1]+">", "@"+b.userName(r[1]), -1)
}
return text
}
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (b *Bslack) replaceChannel(text string) string {
results := regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`).FindAllStringSubmatch(text, -1)
for _, r := range results {
text = strings.Replace(text, r[0], "#"+r[1], -1)
}
return text
}
// @see https://api.slack.com/docs/message-formatting#variables
func (b *Bslack) replaceVariable(text string) string {
results := regexp.MustCompile(`<!([a-zA-Z0-9]+)(\|.+?)?>`).FindAllStringSubmatch(text, -1)
for _, r := range results {
text = strings.Replace(text, r[0], "@"+r[1], -1)
}
return text
}
// @see https://api.slack.com/docs/message-formatting#linking_to_urls
func (b *Bslack) replaceURL(text string) string {
results := regexp.MustCompile(`<(.*?)(\|.*?)?>`).FindAllStringSubmatch(text, -1)
for _, r := range results {
text = strings.Replace(text, r[0], r[1], -1)
}
return text
}
func (b *Bslack) createAttach(extra map[string][]interface{}) []slack.Attachment {
var attachs []slack.Attachment
for _, v := range extra["attachments"] {
entry := v.(map[string]interface{})
s := slack.Attachment{}
s.Fallback = entry["fallback"].(string)
s.Color = entry["color"].(string)
s.Pretext = entry["pretext"].(string)
s.AuthorName = entry["author_name"].(string)
s.AuthorLink = entry["author_link"].(string)
s.AuthorIcon = entry["author_icon"].(string)
s.Title = entry["title"].(string)
s.TitleLink = entry["title_link"].(string)
s.Text = entry["text"].(string)
s.ImageURL = entry["image_url"].(string)
s.ThumbURL = entry["thumb_url"].(string)
s.Footer = entry["footer"].(string)
s.FooterIcon = entry["footer_icon"].(string)
attachs = append(attachs, s)
}
return attachs
}
func (b *Bslack) downloadFile(url string) (*[]byte, error) {
var buf bytes.Buffer
client := &http.Client{
Timeout: time.Second * 5,
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+b.Config.Token)
resp, err := client.Do(req)
if err != nil {
resp.Body.Close()
return nil, err
}
io.Copy(&buf, resp.Body)
data := buf.Bytes()
resp.Body.Close()
return &data, nil
}

View File

@ -2,57 +2,48 @@ package bsshchat
import ( import (
"bufio" "bufio"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/shazow/ssh-chat/sshd"
"io" "io"
"strings" "strings"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/shazow/ssh-chat/sshd"
) )
type Bsshchat struct { type Bsshchat struct {
r *bufio.Scanner r *bufio.Scanner
w io.WriteCloser w io.WriteCloser
*bridge.Config *config.BridgeConfig
} }
func New(cfg *bridge.Config) bridge.Bridger { var flog *log.Entry
return &Bsshchat{Config: cfg} var protocol = "sshchat"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bsshchat {
return &Bsshchat{BridgeConfig: cfg}
} }
func (b *Bsshchat) Connect() error { func (b *Bsshchat) Connect() error {
b.Log.Infof("Connecting %s", b.GetString("Server")) var err error
flog.Infof("Connecting %s", b.Config.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() { go func() {
// As a successful connection will result in this returning after the Connection err = sshd.ConnectShell(b.Config.Server, b.Config.Nick, func(r io.Reader, w io.WriteCloser) error {
// method has already returned point we NEED to have a buffered channel to still b.r = bufio.NewScanner(r)
// be able to write. b.w = w
connErr <- sshd.ConnectShell(b.GetString("Server"), b.GetString("Nick"), connHandler) b.r.Scan()
w.Write([]byte("/theme mono\r\n"))
b.handleSshChat()
return nil
})
}() }()
if err != nil {
select { flog.Debugf("%#v", err)
case err := <-connErr:
b.Log.Error("Connection failed")
return err return err
case <-connSignal:
} }
b.Log.Info("Connection succeeded") flog.Info("Connection succeeded")
return nil return nil
} }
@ -66,22 +57,24 @@ func (b *Bsshchat) JoinChannel(channel config.ChannelInfo) error {
func (b *Bsshchat) Send(msg config.Message) (string, error) { func (b *Bsshchat) Send(msg config.Message) (string, error) {
// ignore delete messages // ignore delete messages
if msg.Event == config.EventMsgDelete { if msg.Event == config.EVENT_MSG_DELETE {
return "", nil return "", nil
} }
b.Log.Debugf("=> Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
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 { if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg) for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text = fi.URL
}
b.w.Write([]byte(msg.Username + msg.Text))
}
return "", nil
} }
} }
_, err := b.w.Write([]byte(msg.Username + msg.Text + "\r\n")) b.w.Write([]byte(msg.Username + msg.Text + "\r\n"))
return "", err return "", nil
} }
/* /*
@ -93,10 +86,10 @@ func (b *Bsshchat) sshchatKeepAlive() chan bool {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
b.Log.Debugf("PING") flog.Debugf("PING")
err := b.xc.PingC2S("", "") err := b.xc.PingC2S("", "")
if err != nil { if err != nil {
b.Log.Debugf("PING failed %#v", err) flog.Debugf("PING failed %#v", err)
} }
case <-done: case <-done:
return return
@ -115,7 +108,7 @@ func stripPrompt(s string) string {
return s[pos+3:] return s[pos+3:]
} }
func (b *Bsshchat) handleSSHChat() error { func (b *Bsshchat) handleSshChat() error {
/* /*
done := b.sshchatKeepAlive() done := b.sshchatKeepAlive()
defer close(done) defer close(done)
@ -123,43 +116,17 @@ func (b *Bsshchat) handleSSHChat() error {
wait := true wait := true
for { for {
if b.r.Scan() { if b.r.Scan() {
// ignore messages from ourselves
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()), ":") res := strings.Split(stripPrompt(b.r.Text()), ":")
if res[0] == "-> Set theme" { if res[0] == "-> Set theme" {
wait = false wait = false
b.Log.Debugf("mono found, allowing") log.Debugf("mono found, allowing")
continue continue
} }
if !wait { if !wait {
b.Log.Debugf("<= Message %#v", res) flog.Debugf("message %#v", res)
rmsg := config.Message{Username: res[0], Text: strings.TrimSpace(strings.Join(res[1:], ":")), Channel: "sshchat", Account: b.Account, UserID: "nick"} rmsg := config.Message{Username: res[0], Text: strings.Join(res[1:], ":"), Channel: "sshchat", Account: b.Account, UserID: "nick"}
b.Remote <- rmsg 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
}

View File

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

View File

@ -2,15 +2,15 @@ package bsteam
import ( import (
"fmt" "fmt"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/Philipp15b/go-steam" "github.com/Philipp15b/go-steam"
"github.com/Philipp15b/go-steam/protocol/steamlang" "github.com/Philipp15b/go-steam/protocol/steamlang"
"github.com/Philipp15b/go-steam/steamid" "github.com/Philipp15b/go-steam/steamid"
log "github.com/Sirupsen/logrus"
//"io/ioutil"
"strconv"
"sync"
"time"
) )
type Bsteam struct { type Bsteam struct {
@ -18,24 +18,31 @@ type Bsteam struct {
connected chan struct{} connected chan struct{}
userMap map[steamid.SteamId]string userMap map[steamid.SteamId]string
sync.RWMutex sync.RWMutex
*bridge.Config *config.BridgeConfig
} }
func New(cfg *bridge.Config) bridge.Bridger { var flog *log.Entry
b := &Bsteam{Config: cfg} var protocol = "steam"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bsteam {
b := &Bsteam{BridgeConfig: cfg}
b.userMap = make(map[steamid.SteamId]string) b.userMap = make(map[steamid.SteamId]string)
b.connected = make(chan struct{}) b.connected = make(chan struct{})
return b return b
} }
func (b *Bsteam) Connect() error { func (b *Bsteam) Connect() error {
b.Log.Info("Connecting") flog.Info("Connecting")
b.c = steam.NewClient() b.c = steam.NewClient()
go b.handleEvents() go b.handleEvents()
go b.c.Connect() go b.c.Connect()
select { select {
case <-b.connected: case <-b.connected:
b.Log.Info("Connection succeeded") flog.Info("Connection succeeded")
case <-time.After(time.Second * 30): case <-time.After(time.Second * 30):
return fmt.Errorf("connection timed out") return fmt.Errorf("connection timed out")
} }
@ -59,28 +66,13 @@ func (b *Bsteam) JoinChannel(channel config.ChannelInfo) error {
func (b *Bsteam) Send(msg config.Message) (string, error) { func (b *Bsteam) Send(msg config.Message) (string, error) {
// ignore delete messages // ignore delete messages
if msg.Event == config.EventMsgDelete { if msg.Event == config.EVENT_MSG_DELETE {
return "", nil return "", nil
} }
id, err := steamid.NewId(msg.Channel) id, err := steamid.NewId(msg.Channel)
if err != nil { if err != nil {
return "", err return "", err
} }
// Handle files
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, rmsg.Username+rmsg.Text)
}
for i := range msg.Extra["file"] {
if err := b.handleFileInfo(&msg, msg.Extra["file"][i]); err != nil {
b.Log.Error(err)
}
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) b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
return "", nil return "", nil
} }
@ -93,3 +85,80 @@ func (b *Bsteam) getNick(id steamid.SteamId) string {
} }
return "unknown" return "unknown"
} }
func (b *Bsteam) handleEvents() {
myLoginInfo := new(steam.LogOnDetails)
myLoginInfo.Username = b.Config.Login
myLoginInfo.Password = b.Config.Password
myLoginInfo.AuthCode = b.Config.AuthCode
// Attempt to read existing auth hash to avoid steam guard.
// Maybe works
//myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry")
for event := range b.c.Events() {
//flog.Info(event)
switch e := event.(type) {
case *steam.ChatMsgEvent:
flog.Debugf("Receiving ChatMsgEvent: %#v", e)
flog.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:
flog.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:
/*
flog.Info("authupdate", e)
flog.Info("hash", e.Hash)
ioutil.WriteFile("sentry", e.Hash, 0666)
*/
case *steam.LogOnFailedEvent:
flog.Info("Logon failed", e)
switch e.Result {
case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode:
{
flog.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:
{
flog.Info("Steam guard isn't letting me in! Enter auth code:")
var code string
fmt.Scanf("%s", &code)
myLoginInfo.AuthCode = code
}
default:
log.Errorf("LogOnFailedEvent: %#v ", e.Result)
// TODO: Handle EResult_InvalidLoginAuthCode
return
}
case *steam.LoggedOnEvent:
flog.Debugf("LoggedOnEvent: %#v", e)
b.connected <- struct{}{}
flog.Debugf("setting online")
b.c.Social.SetPersonaState(steamlang.EPersonaState_Online)
case *steam.DisconnectedEvent:
flog.Info("Disconnected")
flog.Info("Attempting to reconnect...")
b.c.Connect()
case steam.FatalErrorEvent:
flog.Error(e)
case error:
flog.Error(e)
default:
flog.Debugf("unknown event %#v", e)
}
}
}

View File

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

View File

@ -2,16 +2,15 @@ package btelegram
import ( import (
"bytes" "bytes"
"html"
"github.com/russross/blackfriday" "github.com/russross/blackfriday"
"html"
) )
type customHTML struct { type customHtml struct {
blackfriday.Renderer blackfriday.Renderer
} }
func (options *customHTML) Paragraph(out *bytes.Buffer, text func() bool) { func (options *customHtml) Paragraph(out *bytes.Buffer, text func() bool) {
marker := out.Len() marker := out.Len()
if !text() { if !text() {
@ -21,32 +20,32 @@ func (options *customHTML) Paragraph(out *bytes.Buffer, text func() bool) {
out.WriteString("\n") out.WriteString("\n")
} }
func (options *customHTML) BlockCode(out *bytes.Buffer, text []byte, lang string) { func (options *customHtml) BlockCode(out *bytes.Buffer, text []byte, lang string) {
out.WriteString("<pre>") out.WriteString("<pre>")
out.WriteString(html.EscapeString(string(text))) out.WriteString(html.EscapeString(string(text)))
out.WriteString("</pre>\n") out.WriteString("</pre>\n")
} }
func (options *customHTML) Header(out *bytes.Buffer, text func() bool, level int, id string) { func (options *customHtml) Header(out *bytes.Buffer, text func() bool, level int, id string) {
options.Paragraph(out, text) options.Paragraph(out, text)
} }
func (options *customHTML) HRule(out *bytes.Buffer) { func (options *customHtml) HRule(out *bytes.Buffer) {
out.WriteByte('\n') //nolint:errcheck out.WriteByte('\n')
} }
func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) { func (options *customHtml) BlockQuote(out *bytes.Buffer, text []byte) {
out.WriteString("> ") out.WriteString("> ")
out.Write(text) out.Write(text)
out.WriteByte('\n') out.WriteByte('\n')
} }
func (options *customHTML) List(out *bytes.Buffer, text func() bool, flags int) { func (options *customHtml) List(out *bytes.Buffer, text func() bool, flags int) {
options.Paragraph(out, text) options.Paragraph(out, text)
} }
func (options *customHTML) ListItem(out *bytes.Buffer, text []byte, flags int) { func (options *customHtml) ListItem(out *bytes.Buffer, text []byte, flags int) {
out.WriteString("- ") out.WriteString("- ")
out.Write(text) out.Write(text)
out.WriteByte('\n') out.WriteByte('\n')
@ -54,7 +53,7 @@ func (options *customHTML) ListItem(out *bytes.Buffer, text []byte, flags int) {
func makeHTML(input string) string { func makeHTML(input string) string {
return string(blackfriday.Markdown([]byte(input), return string(blackfriday.Markdown([]byte(input),
&customHTML{blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML|blackfriday.HTML_SKIP_IMAGES, "", "")}, &customHtml{blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML|blackfriday.HTML_SKIP_IMAGES, "", "")},
blackfriday.EXTENSION_NO_INTRA_EMPHASIS| blackfriday.EXTENSION_NO_INTRA_EMPHASIS|
blackfriday.EXTENSION_FENCED_CODE| blackfriday.EXTENSION_FENCED_CODE|
blackfriday.EXTENSION_AUTOLINK| blackfriday.EXTENSION_AUTOLINK|

View File

@ -1,54 +1,53 @@
package btelegram package btelegram
import ( import (
"html" "regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
log "github.com/Sirupsen/logrus"
"github.com/go-telegram-bot-api/telegram-bot-api" "github.com/go-telegram-bot-api/telegram-bot-api"
) )
const (
unknownUser = "unknown"
HTMLFormat = "HTML"
HTMLNick = "htmlnick"
)
type Btelegram struct { type Btelegram struct {
c *tgbotapi.BotAPI c *tgbotapi.BotAPI
*bridge.Config *config.BridgeConfig
avatarMap map[string]string // keep cache of userid and avatar sha
} }
func New(cfg *bridge.Config) bridge.Bridger { var flog *log.Entry
return &Btelegram{Config: cfg, avatarMap: make(map[string]string)} var protocol = "telegram"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Btelegram {
return &Btelegram{BridgeConfig: cfg}
} }
func (b *Btelegram) Connect() error { func (b *Btelegram) Connect() error {
var err error var err error
b.Log.Info("Connecting") flog.Info("Connecting")
b.c, err = tgbotapi.NewBotAPI(b.GetString("Token")) b.c, err = tgbotapi.NewBotAPI(b.Config.Token)
if err != nil { if err != nil {
b.Log.Debugf("%#v", err) flog.Debugf("%#v", err)
return err return err
} }
u := tgbotapi.NewUpdate(0) updates, err := b.c.GetUpdatesChan(tgbotapi.NewUpdate(0))
u.Timeout = 60
updates, err := b.c.GetUpdatesChan(u)
if err != nil { if err != nil {
b.Log.Debugf("%#v", err) flog.Debugf("%#v", err)
return err return err
} }
b.Log.Info("Connection succeeded") flog.Info("Connection succeeded")
go b.handleRecv(updates) go b.handleRecv(updates)
return nil return nil
} }
func (b *Btelegram) Disconnect() error { func (b *Btelegram) Disconnect() error {
return nil return nil
} }
func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error { func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error {
@ -56,48 +55,178 @@ func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error {
} }
func (b *Btelegram) Send(msg config.Message) (string, error) { func (b *Btelegram) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
// get the chatid
chatid, err := strconv.ParseInt(msg.Channel, 10, 64) chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
if err != nil { if err != nil {
return "", err return "", err
} }
// map the file SHA to our user (caches the avatar) if b.Config.MessageFormat == "HTML" {
if msg.Event == config.EventAvatarDownload {
return b.cacheAvatar(&msg)
}
if b.GetString("MessageFormat") == HTMLFormat {
msg.Text = makeHTML(msg.Text) msg.Text = makeHTML(msg.Text)
} }
// Delete message if msg.Event == config.EVENT_MSG_DELETE {
if msg.Event == config.EventMsgDelete { if msg.ID == "" {
return b.handleDelete(&msg, chatid) return "", nil
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
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) msgid, err := strconv.Atoi(msg.ID)
if len(msg.Extra["file"]) > 0 { if err != nil {
b.handleUploadFile(&msg, chatid) return "", err
} }
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
return "", err
} }
// edit the message if we have a msg ID // edit the message if we have a msg ID
if msg.ID != "" { if msg.ID != "" {
return b.handleEdit(&msg, chatid) msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
if b.Config.MessageFormat == "HTML" {
m.ParseMode = tgbotapi.ModeHTML
}
_, err = b.c.Send(m)
if err != nil {
return "", err
}
return "", nil
} }
// Post normal message if msg.Extra != nil {
return b.sendMessage(chatid, msg.Username, msg.Text) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
var c tgbotapi.Chattable
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := tgbotapi.FileBytes{fi.Name, *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 {
log.Errorf("file upload failed: %#v", err)
}
if fi.Comment != "" {
b.sendMessage(chatid, msg.Username+fi.Comment)
}
}
return "", nil
}
}
return b.sendMessage(chatid, msg.Username+msg.Text)
}
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
for update := range updates {
flog.Debugf("Receiving from telegram: %#v", update.Message)
var message *tgbotapi.Message
username := ""
channel := ""
text := ""
fmsg := config.Message{Extra: make(map[string][]interface{})}
// handle channels
if update.ChannelPost != nil {
message = update.ChannelPost
}
if update.EditedChannelPost != nil && !b.Config.EditDisable {
message = update.EditedChannelPost
message.Text = message.Text + b.Config.EditSuffix
}
// handle groups
if update.Message != nil {
message = update.Message
}
if update.EditedMessage != nil && !b.Config.EditDisable {
message = update.EditedMessage
message.Text = message.Text + b.Config.EditSuffix
}
if message.From != nil {
if b.Config.UseFirstName {
username = message.From.FirstName
}
if username == "" {
username = message.From.UserName
if username == "" {
username = message.From.FirstName
}
}
text = message.Text
channel = strconv.FormatInt(message.Chat.ID, 10)
}
if username == "" {
username = "unknown"
}
if message.Sticker != nil {
b.handleDownload(message.Sticker, &fmsg)
}
if message.Video != nil {
b.handleDownload(message.Video, &fmsg)
}
if message.Photo != nil {
b.handleDownload(message.Photo, &fmsg)
}
if message.Document != nil {
b.handleDownload(message.Document, &fmsg)
}
if message.Voice != nil {
b.handleDownload(message.Voice, &fmsg)
}
if message.Audio != nil {
b.handleDownload(message.Audio, &fmsg)
}
if message.ForwardFrom != nil {
usernameForward := ""
if b.Config.UseFirstName {
usernameForward = message.ForwardFrom.FirstName
}
if usernameForward == "" {
usernameForward = message.ForwardFrom.UserName
if usernameForward == "" {
usernameForward = message.ForwardFrom.FirstName
}
}
if usernameForward == "" {
usernameForward = "unknown"
}
text = "Forwarded from " + usernameForward + ": " + text
}
// quote the previous message
if message.ReplyToMessage != nil {
usernameReply := ""
if message.ReplyToMessage.From != nil {
if b.Config.UseFirstName {
usernameReply = message.ReplyToMessage.From.FirstName
}
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.UserName
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.FirstName
}
}
}
if usernameReply == "" {
usernameReply = "unknown"
}
text = text + " (re @" + usernameReply + ":" + message.ReplyToMessage.Text + ")"
}
if text != "" || len(fmsg.Extra) > 0 {
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID), Extra: fmsg.Extra}
flog.Debugf("Message is %#v", msg)
b.Remote <- msg
}
}
} }
func (b *Btelegram) getFileDirectURL(id string) string { func (b *Btelegram) getFileDirectURL(id string) string {
@ -108,20 +237,82 @@ func (b *Btelegram) getFileDirectURL(id string) string {
return res return res
} }
func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) { func (b *Btelegram) handleDownload(file interface{}, msg *config.Message) {
m := tgbotapi.NewMessage(chatid, "") size := 0
m.Text = username + text url := ""
if b.GetString("MessageFormat") == HTMLFormat { name := ""
b.Log.Debug("Using mode HTML") text := ""
m.ParseMode = tgbotapi.ModeHTML fileid := ""
switch v := file.(type) {
case *tgbotapi.Audio:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
fileid = v.FileID
case *tgbotapi.Voice:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
if !strings.HasSuffix(name, ".ogg") {
name = name + ".ogg"
}
fileid = v.FileID
case *tgbotapi.Sticker:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
if !strings.HasSuffix(name, ".webp") {
name = name + ".webp"
}
text = " " + url
fileid = v.FileID
case *tgbotapi.Video:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
text = " " + url
fileid = v.FileID
case *[]tgbotapi.PhotoSize:
photos := *v
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
case *tgbotapi.Document:
size = v.FileSize
url = b.getFileDirectURL(v.FileID)
name = v.FileName
text = " " + v.FileName + " : " + url
fileid = v.FileID
} }
if b.GetString("MessageFormat") == "Markdown" { if b.Config.UseInsecureURL {
b.Log.Debug("Using mode markdown") msg.Text = text
m.ParseMode = tgbotapi.ModeMarkdown return
} }
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
b.Log.Debug("Using mode HTML - nick only") // limit to 1MB for now
m.Text = username + html.EscapeString(text) flog.Debugf("trying to download %#v fileid %#v with size %#v", name, fileid, size)
if size <= b.General.MediaDownloadSize {
data, err := helper.DownloadFile(url)
if err != nil {
flog.Errorf("download %s failed %#v", url, err)
} else {
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data})
}
}
}
func (b *Btelegram) sendMessage(chatid int64, text string) (string, error) {
m := tgbotapi.NewMessage(chatid, text)
if b.Config.MessageFormat == "HTML" {
m.ParseMode = tgbotapi.ModeHTML m.ParseMode = tgbotapi.ModeHTML
} }
res, err := b.c.Send(m) res, err := b.c.Send(m)
@ -130,14 +321,3 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, er
} }
return strconv.Itoa(res.MessageID), nil return strconv.Itoa(res.MessageID), nil
} }
func (b *Btelegram) 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
}

View File

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

View File

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

View File

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

View File

@ -2,39 +2,43 @@ package bxmpp
import ( import (
"crypto/tls" "crypto/tls"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
"github.com/jpillora/backoff"
"github.com/mattn/go-xmpp"
"strings" "strings"
"time" "time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/jpillora/backoff"
"github.com/matterbridge/go-xmpp"
"github.com/rs/xid"
) )
type Bxmpp struct { type Bxmpp struct {
xc *xmpp.Client xc *xmpp.Client
xmppMap map[string]string xmppMap map[string]string
*bridge.Config *config.BridgeConfig
startTime time.Time
} }
func New(cfg *bridge.Config) bridge.Bridger { var flog *log.Entry
b := &Bxmpp{Config: cfg} var protocol = "xmpp"
func init() {
flog = log.WithFields(log.Fields{"module": protocol})
}
func New(cfg *config.BridgeConfig) *Bxmpp {
b := &Bxmpp{BridgeConfig: cfg}
b.xmppMap = make(map[string]string) b.xmppMap = make(map[string]string)
return b return b
} }
func (b *Bxmpp) Connect() error { func (b *Bxmpp) Connect() error {
var err error var err error
b.Log.Infof("Connecting %s", b.GetString("Server")) flog.Infof("Connecting %s", b.Config.Server)
b.xc, err = b.createXMPP() b.xc, err = b.createXMPP()
if err != nil { if err != nil {
b.Log.Debugf("%#v", err) flog.Debugf("%#v", err)
return err return err
} }
b.Log.Info("Connection succeeded") flog.Info("Connection succeeded")
go func() { go func() {
initial := true initial := true
bf := &backoff.Backoff{ bf := &backoff.Backoff{
@ -44,16 +48,16 @@ func (b *Bxmpp) Connect() error {
} }
for { for {
if initial { if initial {
b.handleXMPP() b.handleXmpp()
initial = false initial = false
} }
d := bf.Duration() d := bf.Duration()
b.Log.Infof("Disconnected. Reconnecting in %s", d) flog.Infof("Disconnected. Reconnecting in %s", d)
time.Sleep(d) time.Sleep(d)
b.xc, err = b.createXMPP() b.xc, err = b.createXMPP()
if err == nil { if err == nil {
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels} b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
b.handleXMPP() b.handleXmpp()
bf.Reset() bf.Reset()
} }
} }
@ -66,64 +70,53 @@ func (b *Bxmpp) Disconnect() error {
} }
func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error { func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error {
if channel.Options.Key != "" { b.xc.JoinMUCNoHistory(channel.Name+"@"+b.Config.Muc, b.Config.Nick)
b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
b.xc.JoinProtectedMUC(channel.Name+"@"+b.GetString("Muc"), b.GetString("Nick"), channel.Options.Key, xmpp.NoHistory, 0, nil)
} else {
b.xc.JoinMUCNoHistory(channel.Name+"@"+b.GetString("Muc"), b.GetString("Nick"))
}
return nil return nil
} }
func (b *Bxmpp) Send(msg config.Message) (string, error) { func (b *Bxmpp) Send(msg config.Message) (string, error) {
// ignore delete messages // ignore delete messages
if msg.Event == config.EventMsgDelete { if msg.Event == config.EVENT_MSG_DELETE {
return "", nil return "", nil
} }
b.Log.Debugf("=> Receiving %#v", msg) flog.Debugf("Receiving %#v", msg)
// Upload a file (in xmpp case send the upload URL because xmpp has no native upload support)
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: rmsg.Channel + "@" + b.GetString("Muc"), Text: rmsg.Username + rmsg.Text})
}
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg) for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text = fi.URL
}
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
}
return "", nil
} }
} }
var msgreplaceid string b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
msgid := xid.New().String() return "", nil
if 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 {
return "", err
}
return msgid, nil
} }
func (b *Bxmpp) createXMPP() (*xmpp.Client, error) { func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
tc := new(tls.Config) tc := new(tls.Config)
tc.InsecureSkipVerify = b.GetBool("SkipTLSVerify") tc.InsecureSkipVerify = b.Config.SkipTLSVerify
tc.ServerName = strings.Split(b.GetString("Server"), ":")[0] tc.ServerName = strings.Split(b.Config.Server, ":")[0]
options := xmpp.Options{ options := xmpp.Options{
Host: b.GetString("Server"), Host: b.Config.Server,
User: b.GetString("Jid"), User: b.Config.Jid,
Password: b.GetString("Password"), Password: b.Config.Password,
NoTLS: true, NoTLS: true,
StartTLS: true, StartTLS: true,
TLSConfig: tc, TLSConfig: tc,
Debug: b.GetBool("debug"),
Logger: b.Log.Writer(), //StartTLS: false,
Debug: true,
Session: true, Session: true,
Status: "", Status: "",
StatusMessage: "", StatusMessage: "",
Resource: "", Resource: "",
InsecureAllowUnencryptedAuth: false, InsecureAllowUnencryptedAuth: false,
//InsecureAllowUnencryptedAuth: true,
} }
var err error var err error
b.xc, err = options.NewClient() b.xc, err = options.NewClient()
@ -138,10 +131,10 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
b.Log.Debugf("PING") flog.Debugf("PING")
err := b.xc.PingC2S("", "") err := b.xc.PingC2S("", "")
if err != nil { if err != nil {
b.Log.Debugf("PING failed %#v", err) flog.Debugf("PING failed %#v", err)
} }
case <-done: case <-done:
return return
@ -151,12 +144,11 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
return done return done
} }
func (b *Bxmpp) handleXMPP() error { func (b *Bxmpp) handleXmpp() error {
var ok bool var ok bool
var msgid string
b.startTime = time.Now()
done := b.xmppKeepAlive() done := b.xmppKeepAlive()
defer close(done) defer close(done)
nodelay := time.Time{}
for { for {
m, err := b.xc.Recv() m, err := b.xc.Recv()
if err != nil { if err != nil {
@ -164,38 +156,25 @@ func (b *Bxmpp) handleXMPP() error {
} }
switch v := m.(type) { switch v := m.(type) {
case xmpp.Chat: case xmpp.Chat:
var channel, nick string
if v.Type == "groupchat" { if v.Type == "groupchat" {
b.Log.Debugf("== Receiving %#v", v) s := strings.Split(v.Remote, "@")
event := "" if len(s) >= 2 {
// skip invalid messages channel = s[0]
if b.skipMessage(v) {
continue
} }
if strings.Contains(v.Text, "has set the subject to:") { s = strings.Split(s[1], "/")
event = config.EventTopicChange if len(s) == 2 {
nick = s[1]
} }
msgid = v.ID if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" {
if v.ReplaceID != "" { rmsg := config.Message{Username: nick, Text: v.Text, Channel: channel, Account: b.Account, UserID: v.Remote}
msgid = v.ReplaceID rmsg.Text, ok = b.replaceAction(rmsg.Text)
if ok {
rmsg.Event = config.EVENT_USER_ACTION
}
flog.Debugf("Sending message from %s on %s to gateway", nick, b.Account)
b.Remote <- rmsg
} }
rmsg := config.Message{
Username: b.parseNick(v.Remote),
Text: v.Text,
Channel: b.parseChannel(v.Remote),
Account: b.Account,
UserID: v.Remote,
ID: msgid,
Event: event,
}
// check if we have an action event
rmsg.Text, ok = b.replaceAction(rmsg.Text)
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: case xmpp.Presence:
// do nothing // do nothing
@ -209,76 +188,3 @@ func (b *Bxmpp) replaceAction(text string) (string, bool) {
} }
return text, false return text, false
} }
// handleUploadFile handles native upload of files
func (b *Bxmpp) handleUploadFile(msg *config.Message) (string, error) {
var urldesc = ""
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
urldesc = fi.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 fi.URL != "" {
b.xc.SendOOB(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Ooburl: fi.URL, Oobdesc: urldesc})
}
}
return "", nil
}
func (b *Bxmpp) parseNick(remote string) string {
s := strings.Split(remote, "@")
if len(s) > 0 {
s = strings.Split(s[1], "/")
if len(s) == 2 {
return s[1] // nick
}
}
return ""
}
func (b *Bxmpp) parseChannel(remote string) string {
s := strings.Split(remote, "@")
if len(s) >= 2 {
return s[0] // channel
}
return ""
}
// skipMessage skips messages that need to be skipped
func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
// skip messages from ourselves
if b.parseNick(message.Remote) == b.GetString("Nick") {
return true
}
// skip empty messages
if message.Text == "" {
return true
}
// skip subject messages
if strings.Contains(message.Text, "</subject>") {
return true
}
// do not show subjects on connect #732
if strings.Contains(message.Text, "has set the subject to:") && time.Since(b.startTime) < time.Second*5 {
return true
}
// skip delayed messages
t := time.Time{}
return message.Stamp != t
}

View File

@ -1,207 +0,0 @@
package bzulip
import (
"encoding/json"
"io/ioutil"
"strconv"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
gzb "github.com/matterbridge/gozulipbot"
)
type Bzulip struct {
q *gzb.Queue
bot *gzb.Bot
streams map[int]string
*bridge.Config
sync.RWMutex
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Bzulip{Config: cfg, streams: make(map[int]string)}
}
func (b *Bzulip) Connect() error {
bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login")}
bot.Init()
q, err := bot.RegisterAll()
b.q = q
b.bot = &bot
if err != nil {
b.Log.Errorf("Connect() %#v", err)
return err
}
// init stream
b.getChannel(0)
b.Log.Info("Connection succeeded")
go b.handleQueue()
return nil
}
func (b *Bzulip) Disconnect() error {
return nil
}
func (b *Bzulip) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Bzulip) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
_, err := b.bot.UpdateMessage(msg.ID, "")
return "", err
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.sendMessage(rmsg)
}
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg)
}
}
// edit the message if we have a msg ID
if msg.ID != "" {
_, err := b.bot.UpdateMessage(msg.ID, msg.Username+msg.Text)
return "", err
}
// Post normal message
return b.sendMessage(msg)
}
func (b *Bzulip) getChannel(id int) string {
if name, ok := b.streams[id]; ok {
return name
}
streams, err := b.bot.GetRawStreams()
if err != nil {
b.Log.Errorf("getChannel: %#v", err)
return ""
}
for _, stream := range streams.Streams {
b.streams[stream.StreamID] = stream.Name
}
if name, ok := b.streams[id]; ok {
return name
}
return ""
}
func (b *Bzulip) handleQueue() error {
for {
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) + "/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
b.q.LastEventID = m.ID
}
time.Sleep(time.Second * 3)
}
}
func (b *Bzulip) sendMessage(msg config.Message) (string, error) {
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,
Topic: topic,
Content: msg.Username + msg.Text,
}
resp, err := b.bot.Message(m)
if err != nil {
return "", err
}
if resp != nil {
defer resp.Body.Close()
res, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var jr struct {
ID int `json:"id"`
}
err = json.Unmarshal(res, &jr)
if err != nil {
return "", err
}
return strconv.Itoa(jr.ID), nil
}
return "", nil
}
func (b *Bzulip) 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
}
}
_, err := b.sendMessage(*msg)
if err != nil {
return "", err
}
}
return "", nil
}

View File

@ -1,297 +1,3 @@
# 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)
## 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: Send notices on join/parts (matrix). Fixes #712 (#716)
* matrix: Detect html nicks in RemoteNickFormat (matrix). Fixes #696 (#719)
* slack: Hint at thread replies when messages are unthreaded (slack) (#684)
* 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
The slack bridge has been split in a `slack-legacy` and `slack` bridge.
If you're still using `legacy tokens` and want to keep using them you'll have to rename `slack` to `slack-legacy` in your configuration. See [wiki](https://github.com/42wim/matterbridge/wiki/Section-Slack-(basic)#legacy-configuration) for more information.
To migrate to the new bot-token based setup you can follow the instructions [here](https://github.com/42wim/matterbridge/wiki/Slack-bot-setup).
Slack legacy tokens may be deprecated by Slack at short notice, so it is STRONGLY recommended to use a proper bot-token instead.
## New features
* general: New {GATEWAY} variable for `RemoteNickFormat` #501. See `RemoteNickFormat` in matterbridge.toml.sample.
* general: New {CHANNEL} variable for `RemoteNickFormat` #515. See `RemoteNickFormat` in matterbridge.toml.sample.
* general: Remove hyphens when auto-loading envvars from viper config #545
* discord: You can mention discord-users from other bridges.
* slack: Preserve threading between Slack instances #529. See `PreserveThreading` in matterbridge.toml.sample.
* slack: Add ability to show when user is typing across Slack bridges #559
* slack: Add rate-limiting
* mattermost: Add support for mattermost [matterbridge plugin](https://github.com/matterbridge/mattermost-plugin)
* api: Respond with message on connect. #550
* api: Add a health endpoint to API #554
## Bugfix
* slack: Refactoring and making it better.
* slack: Restore file comments coming from Slack. #583
* irc: Fix IRC line splitting. #587
* mattermost: Fix cookie and personal token behaviour. #530
* mattermost: Check for expiring sessions and reconnect.
## Contributors
This release couldn't exist without the following contributors:
@jheiselman, @NikkyAI, @dajohi, @NetwideRogue, @patcon and @Helcaraxan
Special thanks to @Helcaraxan and @patcon for their work on improving/refactoring slack.
# v1.11.3
## Bugfix
* mattermost: fix panic when using webhooks #491
* slack: fix issues regarding API changes and lots of channels #489
* irc: fix rejoin on kick problem #488
# v1.11.2
## Bugfix
* slack: fix slack API changes regarding to files/images
# v1.11.1
## New features
* slack: Add support for slack channels by ID. Closes #436
* discord: Clip too long messages sent to discord (discord). Closes #440
## Bugfix
* general: fix possible panic on downloads that are too big #448
* general: Fix avatar uploads to work with MediaDownloadPath. Closes #454
* discord: allow receiving of topic changes/channel leave/joins from other bridges through the webhook
* discord: Add a space before url in file uploads (discord). Closes #461
* discord: Skip empty messages being sent with the webhook (discord). #469
* mattermost: Use nickname instead of username if defined (mattermost). Closes #452
* irc: Stop numbers being stripped after non-color control codes (irc) (#465)
* slack: Use UserID to look for avatar instead of username (slack). Closes #472
# v1.11.0
## New features
* general: Add config option MediaDownloadPath (#443). See `MediaDownloadPath` in matterbridge.toml.sample
* general: Add MediaDownloadBlacklist option. Closes #442. See `MediaDownloadBlacklist` in matterbridge.toml.sample
* xmpp: Add channel password support for XMPP (#451)
* xmpp: Add message correction support for XMPP (#437)
* telegram: Add support for MessageFormat=htmlnick (telegram). #444
* mattermost: Add support for mattermost 5.x
## Enhancements
* slack: Add Title from attachment slack message (#446)
* irc: Prevent white or black color codes (irc) (#434)
## Bugfix
* slack: Fix regexp in replaceMention (slack). (#435)
* irc: Reconnect on quit. (irc) See #431 (#445)
* sshchat: Ignore messages from ourself. (sshchat) Closes #439
# v1.10.1
## New features
* irc: Colorize username sent to IRC using its crc32 IEEE checksum (#423). See `ColorNicks` in matterbridge.toml.sample
* irc: Add support for CJK to/from utf-8 (irc). #400
* telegram: Add QuoteFormat option (telegram). Closes #413. See `QuoteFormat` in matterbridge.toml.sample
* xmpp: Send attached files to XMPP in different message with OOB data and without body (#421)
## Bugfix
* general: updated irc/xmpp/telegram libraries
* mattermost/slack/rocketchat: Fix iconurl regression. Closes #430
* mattermost/slack: Use uuid instead of userid. Fixes #429
* slack: Avatar spoofing from Slack to Discord with uppercase in nick doesn't work (#433)
* irc: Fix format string bug (irc) (#428)
# v1.10.0
## New features
* general: Add support for reloading all settings automatically after changing config except connection and gateway configuration. Closes #373
* zulip: New protocol support added (https://zulipchat.com)
## Enhancements
* general: Handle file comment better
* steam: Handle file uploads to mediaserver (steam)
* slack: Properly set Slack user who initiated slash command (#394)
## Bugfix
* general: Use only alphanumeric for file uploads to mediaserver. Closes #416
* general: Fix crash on invalid filenames
* general: Fix regression in ReplaceMessages and ReplaceNicks. Closes #407
* telegram: Fix possible nil when using channels (telegram). #410
* telegram: Fix panic (telegram). Closes #410
* telegram: Handle channel posts correctly
* mattermost: Update GetFileLinks to API_V4
# v1.9.1
## New features
* telegram: Add QuoteDisable option (telegram). Closes #399. See QuoteDisable in matterbridge.toml.sample
## Enhancements
* discord: Send mediaserver link to Discord in Webhook mode (discord) (#405)
* mattermost: Print list of valid team names when team not found (#390)
* slack: Strip markdown URLs with blank text (slack) (#392)
## Bugfix
* slack/mattermost: Make our callbackid more unique. Fixes issue with running multiple matterbridge on the same channel (slack,mattermost)
* telegram: fix newlines in multiline messages #399
* telegram: Revert #378
# v1.9.0 (the refactor release)
## New features
* general: better debug messages
* general: better support for environment variables override
* general: Ability to disable sending join/leave messages to other gateways. #382
* slack: Allow Slack @usergroups to be parsed as human-friendly names #379
* slack: Provide better context for shared posts from Slack<=>Slack enhancement #369
* telegram: Convert nicks automatically into HTML when MessageFormat is set to HTML #378
* irc: Add DebugLevel option
## Bugfix
* slack: Ignore restricted_action on channel join (slack). Closes #387
* slack: Add slack attachment support to matterhook
* slack: Update userlist on join (slack). Closes #372
# v1.8.0
## New features
* general: Send chat notification if media is too big to be re-uploaded to MediaServer. See #359
* general: Download (and upload) avatar images from mattermost and telegram when mediaserver is configured. Closes #362
* general: Add label support in RemoteNickFormat
* general: Prettier info/debug log output
* mattermost: Download files and reupload to supported bridges (mattermost). Closes #357
* slack: Add ShowTopicChange option. Allow/disable topic change messages (currently only from slack). Closes #353
* slack: Add support for file comments (slack). Closes #346
* telegram: Add comment to file upload from telegram. Show comments on all bridges. Closes #358
* telegram: Add markdown support (telegram). #355
* api: Give api access to whole config.Message (and events). Closes #374
## Bugfix
* discord: Check for a valid WebhookURL (discord). Closes #367
* discord: Fix role mention replace issues
* irc: Truncate messages sent to IRC based on byte count (#368)
* mattermost: Add file download urls also to mattermost webhooks #356
* telegram: Fix panic on nil messages (telegram). Closes #366
* telegram: Fix the UseInsecureURL text (telegram). Closes #184
# v1.7.1
## Bugfix
* telegram: Enable Long Polling for Telegram. Reduces bandwidth consumption. (#350)
# v1.7.0
## New features
* matrix: Add support for deleting messages from/to matrix (matrix). Closes #320
* xmpp: Ignore <subject> messages (xmpp). #272
* irc: Add twitch support (irc) to README / wiki
## Bugfix
* general: Change RemoteNickFormat replacement order. Closes #336
* general: Make edits/delete work for bridges that gets reused. Closes #342
* general: Lowercase irc channels in config. Closes #348
* matrix: Fix possible panics (matrix). Closes #333
* matrix: Add an extension to images without one (matrix). #331
* api: Obey the Gateway value from the json (api). Closes #344
* xmpp: Print only debug messages when specified (xmpp). Closes #345
* xmpp: Allow xmpp to receive the extra messages (file uploads) when text is empty. #295
# v1.6.3
## Bugfix
* slack: Fix connection issues
* slack: Add more debug messages
* irc: Convert received IRC channel names to lowercase. Fixes #329 (#330)
# v1.6.2
## Bugfix
* mattermost: Crashes while connecting to Mattermost (regression). Closes #327
# v1.6.1 # v1.6.1
## Bugfix ## Bugfix
* general: Display of nicks not longer working (regression). Closes #323 * general: Display of nicks not longer working (regression). Closes #323

View File

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

View File

@ -1,210 +0,0 @@
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: []

View File

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

View File

@ -1,9 +0,0 @@
FROM alpine:edge as certs
RUN apk --update add ca-certificates
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"]

View File

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

View File

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

View File

@ -1,23 +1,23 @@
package gateway package gateway
import ( import (
"io/ioutil" "bytes"
"os" "fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
log "github.com/Sirupsen/logrus"
// "github.com/davecgh/go-spew/spew"
"crypto/sha1"
"github.com/hashicorp/golang-lru"
"github.com/peterhellberg/emojilib"
"net/http"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/d5/tengo/script"
lru "github.com/hashicorp/golang-lru"
"github.com/peterhellberg/emojilib"
"github.com/sirupsen/logrus"
) )
type Gateway struct { type Gateway struct {
config.Config *config.Config
Router *Router Router *Router
MyConfig *config.Gateway MyConfig *config.Gateway
Bridges map[string]*bridge.Bridge Bridges map[string]*bridge.Bridge
@ -26,91 +26,37 @@ type Gateway struct {
Message chan config.Message Message chan config.Message
Name string Name string
Messages *lru.Cache Messages *lru.Cache
logger *logrus.Entry
} }
type BrMsgID struct { type BrMsgID struct {
br *bridge.Bridge br *bridge.Bridge
ID string ID string
ChannelID string
} }
const apiProtocol = "api" func New(cfg config.Gateway, r *Router) *Gateway {
gw := &Gateway{Channels: make(map[string]*config.ChannelInfo), Message: r.Message,
// New creates a new Gateway object associated with the specified router and Router: r, Bridges: make(map[string]*bridge.Bridge), Config: r.Config}
// following the given configuration.
func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway {
logger := rootLogger.WithFields(logrus.Fields{"prefix": "gateway"})
cache, _ := lru.New(5000) cache, _ := lru.New(5000)
gw := &Gateway{ gw.Messages = cache
Channels: make(map[string]*config.ChannelInfo), gw.AddConfig(&cfg)
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 return gw
} }
// 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) {
return mID
}
// If not keyed, iterate through cache for downstream, and infer upstream.
for _, mid := range gw.Messages.Keys() {
v, _ := gw.Messages.Peek(mid)
ids := v.([]*BrMsgID)
for _, downstreamMsgObj := range ids {
if ID == downstreamMsgObj.ID {
return strings.Replace(mid.(string), protocol+" ", "", 1)
}
}
}
return ""
}
// AddBridge sets up a new bridge in the gateway object with the specified configuration.
func (gw *Gateway) AddBridge(cfg *config.Bridge) error { func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
br := gw.Router.getBridge(cfg.Account) br := gw.Router.getBridge(cfg.Account)
if br == nil { if br == nil {
br = bridge.New(cfg) br = bridge.New(gw.Config, cfg, gw.Message)
br.Config = gw.Router.Config
br.General = &gw.BridgeValues().General
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
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.mapChannelsToBridge(br)
gw.Bridges[cfg.Account] = br gw.Bridges[cfg.Account] = br
return nil return nil
} }
// AddConfig associates a new configuration with the gateway object.
func (gw *Gateway) AddConfig(cfg *config.Gateway) error { func (gw *Gateway) AddConfig(cfg *config.Gateway) error {
gw.Name = cfg.Name gw.Name = cfg.Name
gw.MyConfig = cfg gw.MyConfig = cfg
if err := gw.mapChannels(); err != nil { gw.mapChannels()
gw.logger.Errorf("mapChannels() failed: %s", err)
}
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) { for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
br := br //scopelint
err := gw.AddBridge(&br) err := gw.AddBridge(&br)
if err != nil { if err != nil {
return err return err
@ -128,51 +74,29 @@ func (gw *Gateway) mapChannelsToBridge(br *bridge.Bridge) {
} }
func (gw *Gateway) reconnectBridge(br *bridge.Bridge) { func (gw *Gateway) reconnectBridge(br *bridge.Bridge) {
if err := br.Disconnect(); err != nil { br.Disconnect()
gw.logger.Errorf("Disconnect() %s failed: %s", br.Account, err)
}
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
RECONNECT: RECONNECT:
gw.logger.Infof("Reconnecting %s", br.Account) log.Infof("Reconnecting %s", br.Account)
err := br.Connect() err := br.Connect()
if err != nil { if err != nil {
gw.logger.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err) log.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
time.Sleep(time.Second * 60) time.Sleep(time.Second * 60)
goto RECONNECT goto RECONNECT
} }
br.Joined = make(map[string]bool) br.Joined = make(map[string]bool)
if err := br.JoinChannels(); err != nil { br.JoinChannels()
gw.logger.Errorf("JoinChannels() %s failed: %s", br.Account, err)
}
} }
func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) { func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) {
for _, br := range cfg { for _, br := range cfg {
if isAPI(br.Account) { if isApi(br.Account) {
br.Channel = apiProtocol br.Channel = "api"
}
// make sure to lowercase irc channels in config #348
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 ID := br.Channel + br.Account
if _, ok := gw.Channels[ID]; !ok { if _, ok := gw.Channels[ID]; !ok {
channel := &config.ChannelInfo{ channel := &config.ChannelInfo{Name: br.Channel, Direction: direction, ID: ID, Options: br.Options, Account: br.Account,
Name: br.Channel, SameChannel: make(map[string]bool)}
Direction: direction,
ID: ID,
Options: br.Options,
Account: br.Account,
SameChannel: make(map[string]bool),
}
channel.SameChannel[gw.Name] = br.SameChannel channel.SameChannel[gw.Name] = br.SameChannel
gw.Channels[channel.ID] = channel gw.Channels[channel.ID] = channel
} else { } else {
@ -194,44 +118,10 @@ func (gw *Gateway) mapChannels() error {
func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []config.ChannelInfo { func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []config.ChannelInfo {
var channels []config.ChannelInfo var channels []config.ChannelInfo
// for messages received from the api check that the gateway is the specified one
if msg.Protocol == apiProtocol && gw.Name != msg.Gateway {
return channels
}
// discord join/leave is for the whole bridge, isn't a per channel join/leave
if msg.Event == config.EventJoinLeave && getProtocol(msg) == "discord" && msg.Channel == "" {
for _, channel := range gw.Channels {
if channel.Account == dest.Account && strings.Contains(channel.Direction, "out") &&
gw.validGatewayDest(msg) {
channels = append(channels, *channel)
}
}
return channels
}
// irc quit is for the whole bridge, isn't a per channel quit.
// channel is empty when we quit
if msg.Event == config.EventJoinLeave && getProtocol(msg) == "irc" && msg.Channel == "" {
// if we only have one channel on this irc bridge it's got to be the sending one.
// don't send it back
if dest.Account == msg.Account && len(dest.Channels) == 1 && dest.Protocol == "irc" {
return channels
}
for _, channel := range gw.Channels {
if channel.Account == dest.Account && strings.Contains(channel.Direction, "out") &&
gw.validGatewayDest(msg) {
channels = append(channels, *channel)
}
}
return channels
}
// if source channel is in only, do nothing // if source channel is in only, do nothing
for _, channel := range gw.Channels { for _, channel := range gw.Channels {
// lookup the channel from the message // lookup the channel from the message
if channel.ID == getChannelID(msg) { if channel.ID == getChannelID(*msg) {
// we only have destinations if the original message is from an "in" (sending) channel // we only have destinations if the original message is from an "in" (sending) channel
if !strings.Contains(channel.Direction, "in") { if !strings.Contains(channel.Direction, "in") {
return channels return channels
@ -240,7 +130,7 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
} }
} }
for _, channel := range gw.Channels { for _, channel := range gw.Channels {
if _, ok := gw.Channels[getChannelID(msg)]; !ok { if _, ok := gw.Channels[getChannelID(*msg)]; !ok {
continue continue
} }
@ -251,44 +141,74 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
} }
continue continue
} }
if strings.Contains(channel.Direction, "out") && channel.Account == dest.Account && gw.validGatewayDest(msg) { if strings.Contains(channel.Direction, "out") && channel.Account == dest.Account && gw.validGatewayDest(msg, channel) {
channels = append(channels, *channel) channels = append(channels, *channel)
} }
} }
return channels return channels
} }
func (gw *Gateway) getDestMsgID(msgID string, dest *bridge.Bridge, channel *config.ChannelInfo) string { func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrMsgID {
if res, ok := gw.Messages.Get(msgID); ok { var brMsgIDs []*BrMsgID
IDs := res.([]*BrMsgID)
for _, id := range IDs { // TODO refactor
// check protocol, bridge name and channelname // only slack now, check will have to be done in the different bridges.
// for people that reuse the same bridge multiple times. see #342 // we need to check if we can't use fallback or text in other bridges
if dest.Protocol == id.br.Protocol && dest.Name == id.br.Name && channel.ID == id.ChannelID { if msg.Extra != nil {
return strings.Replace(id.ID, dest.Protocol+" ", "", 1) if dest.Protocol != "discord" &&
dest.Protocol != "slack" &&
dest.Protocol != "mattermost" &&
dest.Protocol != "telegram" &&
dest.Protocol != "matrix" {
if msg.Text == "" {
return brMsgIDs
} }
} }
} }
return "" // only relay join/part when configged
} if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart {
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
} }
if msg.Event == config.EventUserTyping { // broadcast to every out channel (irc QUIT)
return false if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE {
log.Debug("empty channel")
return brMsgIDs
} }
// we have an attachment or actual bytes, do not ignore originchannel := msg.Channel
if msg.Extra != nil && origmsg := msg
(msg.Extra["attachments"] != nil || channels := gw.getDestChannel(&msg, *dest)
len(msg.Extra["file"]) > 0 || for _, channel := range channels {
len(msg.Extra[config.EventFileFailureSize]) > 0) { // do not send to ourself
return false if channel.ID == getChannelID(origmsg) {
continue
}
log.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 = ""
if res, ok := gw.Messages.Get(origmsg.ID); ok {
IDs := res.([]*BrMsgID)
for _, id := range IDs {
if dest.Protocol == id.br.Protocol {
msg.ID = id.ID
}
}
}
// for api we need originchannel as channel
if dest.Protocol == "api" {
msg.Channel = originchannel
}
mID, err := dest.Send(msg)
if err != nil {
fmt.Println(err)
}
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
if mID != "" {
brMsgIDs = append(brMsgIDs, &BrMsgID{dest, mID})
}
} }
gw.logger.Debugf("ignoring empty message %#v from %s", msg, msg.Account) return brMsgIDs
return true
} }
func (gw *Gateway) ignoreMessage(msg *config.Message) bool { func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
@ -296,33 +216,57 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
if _, ok := gw.Bridges[msg.Account]; !ok { if _, ok := gw.Bridges[msg.Account]; !ok {
return true return true
} }
if msg.Text == "" {
igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks")) // we have an attachment or actual bytes
igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages")) if msg.Extra != nil && (msg.Extra["attachments"] != nil || len(msg.Extra["file"]) > 0) {
if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) { return false
}
log.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
return true return true
} }
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreNicks) {
if msg.Username == entry {
log.Debugf("ignoring %s from %s", msg.Username, msg.Account)
return true
}
}
// TODO do not compile regexps everytime
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreMessages) {
if entry != "" {
re, err := regexp.Compile(entry)
if err != nil {
log.Errorf("incorrect regexp %s for %s", entry, msg.Account)
continue
}
if re.MatchString(msg.Text) {
log.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account)
return true
}
}
}
return false 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] br := gw.Bridges[msg.Account]
msg.Protocol = br.Protocol msg.Protocol = br.Protocol
if dest.GetBool("StripNick") { if gw.Config.General.StripNick || dest.Config.StripNick {
re := regexp.MustCompile("[^a-zA-Z0-9]+") re := regexp.MustCompile("[^a-zA-Z0-9]+")
msg.Username = re.ReplaceAllString(msg.Username, "") msg.Username = re.ReplaceAllString(msg.Username, "")
} }
nick := dest.GetString("RemoteNickFormat") nick := dest.Config.RemoteNickFormat
if nick == "" {
nick = gw.Config.General.RemoteNickFormat
}
// loop to replace nicks // loop to replace nicks
for _, outer := range br.GetStringSlice2D("ReplaceNicks") { for _, outer := range br.Config.ReplaceNicks {
search := outer[0] search := outer[0]
replace := outer[1] replace := outer[1]
// TODO move compile to bridge init somewhere // TODO move compile to bridge init somewhere
re, err := regexp.Compile(search) re, err := regexp.Compile(search)
if err != nil { if err != nil {
gw.logger.Errorf("regexp in %s failed: %s", msg.Account, err) log.Errorf("regexp in %s failed: %s", msg.Account, err)
break break
} }
msg.Username = re.ReplaceAllString(msg.Username, replace) msg.Username = re.ReplaceAllString(msg.Username, replace)
@ -340,18 +284,17 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri
} }
nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1) nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
} }
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1) nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1) nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
nick = strings.Replace(nick, "{GATEWAY}", gw.Name, -1)
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)
return nick return nick
} }
func (gw *Gateway) modifyAvatar(msg *config.Message, dest *bridge.Bridge) string { func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string {
iconurl := dest.GetString("IconURL") iconurl := gw.Config.General.IconURL
if iconurl == "" {
iconurl = dest.Config.IconURL
}
iconurl = strings.Replace(iconurl, "{NICK}", msg.Username, -1) iconurl = strings.Replace(iconurl, "{NICK}", msg.Username, -1)
if msg.Avatar == "" { if msg.Avatar == "" {
msg.Avatar = iconurl msg.Avatar = iconurl
@ -360,161 +303,60 @@ func (gw *Gateway) modifyAvatar(msg *config.Message, dest *bridge.Bridge) string
} }
func (gw *Gateway) modifyMessage(msg *config.Message) { func (gw *Gateway) modifyMessage(msg *config.Message) {
if err := modifyMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil {
gw.logger.Errorf("TengoModifyMessage failed: %s", err)
}
// replace :emoji: to unicode // replace :emoji: to unicode
msg.Text = emojilib.Replace(msg.Text) msg.Text = emojilib.Replace(msg.Text)
br := gw.Bridges[msg.Account] br := gw.Bridges[msg.Account]
// loop to replace messages // loop to replace messages
for _, outer := range br.GetStringSlice2D("ReplaceMessages") { for _, outer := range br.Config.ReplaceMessages {
search := outer[0] search := outer[0]
replace := outer[1] replace := outer[1]
// TODO move compile to bridge init somewhere // TODO move compile to bridge init somewhere
re, err := regexp.Compile(search) re, err := regexp.Compile(search)
if err != nil { if err != nil {
gw.logger.Errorf("regexp in %s failed: %s", msg.Account, err) log.Errorf("regexp in %s failed: %s", msg.Account, err)
break break
} }
msg.Text = re.ReplaceAllString(msg.Text, replace) msg.Text = re.ReplaceAllString(msg.Text, replace)
} }
msg.Gateway = gw.Name
gw.handleExtractNicks(msg)
// messages from api have Gateway specified, don't overwrite
if msg.Protocol != apiProtocol {
msg.Gateway = gw.Name
}
} }
// SendMessage sends a message (with specified parentID) to the channel on the selected func (gw *Gateway) handleFiles(msg *config.Message) {
// destination bridge and returns a message ID or an error. if msg.Extra == nil || gw.Config.General.MediaServerUpload == "" {
func (gw *Gateway) SendMessage( return
rmsg *config.Message, }
dest *bridge.Bridge, if len(msg.Extra["file"]) > 0 {
channel *config.ChannelInfo, client := &http.Client{
canonicalParentMsgID string, Timeout: time.Second * 5,
) (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 { for i, f := range msg.Extra["file"] {
// do not send to ourself for any other event fi := f.(config.FileInfo)
if channel.ID == getChannelID(rmsg) { sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))
return "", nil reader := bytes.NewReader(*fi.Data)
url := gw.Config.General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
durl := gw.Config.General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
extra := msg.Extra["file"][i].(config.FileInfo)
extra.URL = durl
msg.Extra["file"][i] = extra
req, _ := http.NewRequest("PUT", url, reader)
req.Header.Set("Content-Type", "binary/octet-stream")
_, err := client.Do(req)
if err != nil {
log.Errorf("mediaserver upload failed: %#v", err)
}
log.Debugf("mediaserver download URL = %s", durl)
} }
} }
// Too noisy to log like other events
if msg.Event != config.EventUserTyping {
gw.logger.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name)
}
msg.Channel = channel.Name
msg.Avatar = gw.modifyAvatar(rmsg, dest)
msg.Username = gw.modifyUsername(rmsg, dest)
msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel)
// for api we need originchannel as channel
if dest.Protocol == apiProtocol {
msg.Channel = rmsg.Channel
}
msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel)
if msg.ParentID == "" {
msg.ParentID = canonicalParentMsgID
}
// if the parentID is still empty and we have a parentID set in the original message
// this means that we didn't find it in the cache so set it "msg-parent-not-found"
if msg.ParentID == "" && rmsg.ParentID != "" {
msg.ParentID = "msg-parent-not-found"
}
// if we are using mattermost plugin account, send messages to MattermostPlugin channel
// that can be picked up by the mattermost matterbridge plugin
if dest.Account == "mattermost.plugin" {
gw.Router.MattermostPlugin <- msg
}
mID, err := dest.Send(msg)
if err != nil {
return mID, err
}
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
if mID != "" {
gw.logger.Debugf("mID %s: %s", dest.Account, mID)
return mID, nil
//brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID})
}
return "", nil
} }
func (gw *Gateway) validGatewayDest(msg *config.Message) bool { func getChannelID(msg config.Message) string {
return msg.Gateway == gw.Name
}
func getChannelID(msg *config.Message) string {
return msg.Channel + msg.Account return msg.Channel + msg.Account
} }
func isAPI(account string) bool { func (gw *Gateway) validGatewayDest(msg *config.Message, channel *config.ChannelInfo) bool {
return msg.Gateway == gw.Name
}
func isApi(account string) bool {
return strings.HasPrefix(account, "api.") return strings.HasPrefix(account, "api.")
} }
// ignoreText returns true if text matches any of the input regexes.
func (gw *Gateway) ignoreText(text string, input []string) bool {
for _, entry := range input {
if entry == "" {
continue
}
// TODO do not compile regexps everytime
re, err := regexp.Compile(entry)
if err != nil {
gw.logger.Errorf("incorrect regexp %s", entry)
continue
}
if re.MatchString(text) {
gw.logger.Debugf("matching %s. ignoring %s", entry, text)
return true
}
}
return false
}
func getProtocol(msg *config.Message) string {
p := strings.Split(msg.Account, ".")
return p[0]
}
func modifyMessageTengo(filename string, msg *config.Message) error {
if filename == "" {
return nil
}
res, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
s := script.New(res)
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel)
c, err := s.Compile()
if err != nil {
return err
}
if err := c.Run(); err != nil {
return err
}
msg.Text = c.Get("msgText").String()
msg.Username = c.Get("msgUsername").String()
return nil
}

View File

@ -2,18 +2,15 @@ package gateway
import ( import (
"fmt" "fmt"
"io/ioutil"
"strconv"
"testing"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway/bridgemap" "github.com/BurntSushi/toml"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "strconv"
"testing"
) )
var testconfig = []byte(` var testconfig = `
[irc.freenode] [irc.freenode]
[mattermost.test] [mattermost.test]
[gitter.42wim] [gitter.42wim]
@ -40,9 +37,9 @@ var testconfig = []byte(`
[[gateway.inout]] [[gateway.inout]]
account="slack.test" account="slack.test"
channel="testing" channel="testing"
`) `
var testconfig2 = []byte(` var testconfig2 = `
[irc.freenode] [irc.freenode]
[mattermost.test] [mattermost.test]
[gitter.42wim] [gitter.42wim]
@ -83,9 +80,8 @@ var testconfig2 = []byte(`
[[gateway.out]] [[gateway.out]]
account = "discord.test" account = "discord.test"
channel = "general2" channel = "general2"
`) `
var testconfig3 = `
var testconfig3 = []byte(`
[irc.zzz] [irc.zzz]
[telegram.zzz] [telegram.zzz]
[slack.zzz] [slack.zzz]
@ -153,26 +149,28 @@ enable=true
[[gateway.inout]] [[gateway.inout]]
account="telegram.zzz" account="telegram.zzz"
channel="--333333333333" channel="--333333333333"
`) `
const ( func maketestRouter(input string) *Router {
ircTestAccount = "irc.zzz" var cfg *config.Config
tgTestAccount = "telegram.zzz" if _, err := toml.Decode(input, &cfg); err != nil {
slackTestAccount = "slack.zzz" fmt.Println(err)
) }
r, err := NewRouter(cfg)
func maketestRouter(input []byte) *Router {
logger := logrus.New()
logger.SetOutput(ioutil.Discard)
cfg := config.NewConfigFromString(logger, input)
r, err := NewRouter(logger, cfg, bridgemap.FullMap)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
return r return r
} }
func TestNewRouter(t *testing.T) { func TestNewRouter(t *testing.T) {
r := maketestRouter(testconfig) var cfg *config.Config
if _, err := toml.Decode(testconfig, &cfg); err != nil {
fmt.Println(err)
}
r, err := NewRouter(cfg)
if err != nil {
fmt.Println(err)
}
assert.Equal(t, 1, len(r.Gateways)) assert.Equal(t, 1, len(r.Gateways))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges)) assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels)) assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
@ -183,27 +181,18 @@ func TestNewRouter(t *testing.T) {
assert.Equal(t, 3, len(r.Gateways["bridge2"].Bridges)) assert.Equal(t, 3, len(r.Gateways["bridge2"].Bridges))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels)) assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
assert.Equal(t, 3, len(r.Gateways["bridge2"].Channels)) assert.Equal(t, 3, len(r.Gateways["bridge2"].Channels))
assert.Equal(t, &config.ChannelInfo{ assert.Equal(t, &config.ChannelInfo{Name: "42wim/testroom", Direction: "out",
Name: "42wim/testroom", ID: "42wim/testroomgitter.42wim", Account: "gitter.42wim",
Direction: "out", SameChannel: map[string]bool{"bridge2": false}},
ID: "42wim/testroomgitter.42wim", r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"])
Account: "gitter.42wim", assert.Equal(t, &config.ChannelInfo{Name: "42wim/testroom", Direction: "in",
SameChannel: map[string]bool{"bridge2": false}, ID: "42wim/testroomgitter.42wim", Account: "gitter.42wim",
}, r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"]) SameChannel: map[string]bool{"bridge1": false}},
assert.Equal(t, &config.ChannelInfo{ r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"])
Name: "42wim/testroom", assert.Equal(t, &config.ChannelInfo{Name: "general", Direction: "inout",
Direction: "in", ID: "generaldiscord.test", Account: "discord.test",
ID: "42wim/testroomgitter.42wim", SameChannel: map[string]bool{"bridge1": false}},
Account: "gitter.42wim", r.Gateways["bridge1"].Channels["generaldiscord.test"])
SameChannel: map[string]bool{"bridge1": false},
}, r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"])
assert.Equal(t, &config.ChannelInfo{
Name: "general",
Direction: "inout",
ID: "generaldiscord.test",
Account: "discord.test",
SameChannel: map[string]bool{"bridge1": false},
}, r.Gateways["bridge1"].Channels["generaldiscord.test"])
} }
func TestGetDestChannel(t *testing.T) { func TestGetDestChannel(t *testing.T) {
@ -212,23 +201,11 @@ func TestGetDestChannel(t *testing.T) {
for _, br := range r.Gateways["bridge1"].Bridges { for _, br := range r.Gateways["bridge1"].Bridges {
switch br.Account { switch br.Account {
case "discord.test": case "discord.test":
assert.Equal(t, []config.ChannelInfo{{ assert.Equal(t, []config.ChannelInfo{{Name: "general", Account: "discord.test", Direction: "inout", ID: "generaldiscord.test", SameChannel: map[string]bool{"bridge1": false}, Options: config.ChannelOptions{Key: ""}}},
Name: "general", r.Gateways["bridge1"].getDestChannel(msg, *br))
Account: "discord.test",
Direction: "inout",
ID: "generaldiscord.test",
SameChannel: map[string]bool{"bridge1": false},
Options: config.ChannelOptions{Key: ""},
}}, r.Gateways["bridge1"].getDestChannel(msg, *br))
case "slack.test": case "slack.test":
assert.Equal(t, []config.ChannelInfo{{ assert.Equal(t, []config.ChannelInfo{{Name: "testing", Account: "slack.test", Direction: "out", ID: "testingslack.test", SameChannel: map[string]bool{"bridge1": false}, Options: config.ChannelOptions{Key: ""}}},
Name: "testing", r.Gateways["bridge1"].getDestChannel(msg, *br))
Account: "slack.test",
Direction: "out",
ID: "testingslack.test",
SameChannel: map[string]bool{"bridge1": false},
Options: config.ChannelOptions{Key: ""},
}}, r.Gateways["bridge1"].getDestChannel(msg, *br))
case "gitter.42wim": case "gitter.42wim":
assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br)) assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
case "irc.freenode": case "irc.freenode":
@ -258,87 +235,35 @@ func TestGetDestChannelAdvanced(t *testing.T) {
} }
switch gw.Name { switch gw.Name {
case "bridge": case "bridge":
if (msg.Channel == "#main" || msg.Channel == "-1111111111111" || msg.Channel == "irc") && if (msg.Channel == "#main" || msg.Channel == "-1111111111111" || msg.Channel == "irc") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz" || msg.Account == "slack.zzz") {
(msg.Account == ircTestAccount || msg.Account == tgTestAccount || msg.Account == slackTestAccount) {
hits[gw.Name]++ hits[gw.Name]++
switch br.Account { switch br.Account {
case ircTestAccount: case "irc.zzz":
assert.Equal(t, []config.ChannelInfo{{ assert.Equal(t, []config.ChannelInfo{{Name: "#main", Account: "irc.zzz", Direction: "inout", ID: "#mainirc.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
Name: "#main", case "telegram.zzz":
Account: ircTestAccount, assert.Equal(t, []config.ChannelInfo{{Name: "-1111111111111", Account: "telegram.zzz", Direction: "inout", ID: "-1111111111111telegram.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
Direction: "inout", case "slack.zzz":
ID: "#mainirc.zzz", assert.Equal(t, []config.ChannelInfo{{Name: "irc", Account: "slack.zzz", Direction: "inout", ID: "ircslack.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
SameChannel: map[string]bool{"bridge": false},
Options: config.ChannelOptions{Key: ""},
}}, channels)
case tgTestAccount:
assert.Equal(t, []config.ChannelInfo{{
Name: "-1111111111111",
Account: tgTestAccount,
Direction: "inout",
ID: "-1111111111111telegram.zzz",
SameChannel: map[string]bool{"bridge": false},
Options: config.ChannelOptions{Key: ""},
}}, channels)
case slackTestAccount:
assert.Equal(t, []config.ChannelInfo{{
Name: "irc",
Account: slackTestAccount,
Direction: "inout",
ID: "ircslack.zzz",
SameChannel: map[string]bool{"bridge": false},
Options: config.ChannelOptions{Key: ""},
}}, channels)
} }
} }
case "bridge2": case "bridge2":
if (msg.Channel == "#main-help" || msg.Channel == "--444444444444") && if (msg.Channel == "#main-help" || msg.Channel == "--444444444444") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") {
(msg.Account == ircTestAccount || msg.Account == tgTestAccount) {
hits[gw.Name]++ hits[gw.Name]++
switch br.Account { switch br.Account {
case ircTestAccount: case "irc.zzz":
assert.Equal(t, []config.ChannelInfo{{ assert.Equal(t, []config.ChannelInfo{{Name: "#main-help", Account: "irc.zzz", Direction: "inout", ID: "#main-helpirc.zzz", SameChannel: map[string]bool{"bridge2": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
Name: "#main-help", case "telegram.zzz":
Account: ircTestAccount, assert.Equal(t, []config.ChannelInfo{{Name: "--444444444444", Account: "telegram.zzz", Direction: "inout", ID: "--444444444444telegram.zzz", SameChannel: map[string]bool{"bridge2": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
Direction: "inout",
ID: "#main-helpirc.zzz",
SameChannel: map[string]bool{"bridge2": false},
Options: config.ChannelOptions{Key: ""},
}}, channels)
case tgTestAccount:
assert.Equal(t, []config.ChannelInfo{{
Name: "--444444444444",
Account: tgTestAccount,
Direction: "inout",
ID: "--444444444444telegram.zzz",
SameChannel: map[string]bool{"bridge2": false},
Options: config.ChannelOptions{Key: ""},
}}, channels)
} }
} }
case "bridge3": case "bridge3":
if (msg.Channel == "#main-telegram" || msg.Channel == "--333333333333") && if (msg.Channel == "#main-telegram" || msg.Channel == "--333333333333") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") {
(msg.Account == ircTestAccount || msg.Account == tgTestAccount) {
hits[gw.Name]++ hits[gw.Name]++
switch br.Account { switch br.Account {
case ircTestAccount: case "irc.zzz":
assert.Equal(t, []config.ChannelInfo{{ assert.Equal(t, []config.ChannelInfo{{Name: "#main-telegram", Account: "irc.zzz", Direction: "inout", ID: "#main-telegramirc.zzz", SameChannel: map[string]bool{"bridge3": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
Name: "#main-telegram", case "telegram.zzz":
Account: ircTestAccount, assert.Equal(t, []config.ChannelInfo{{Name: "--333333333333", Account: "telegram.zzz", Direction: "inout", ID: "--333333333333telegram.zzz", SameChannel: map[string]bool{"bridge3": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
Direction: "inout",
ID: "#main-telegramirc.zzz",
SameChannel: map[string]bool{"bridge3": false},
Options: config.ChannelOptions{Key: ""},
}}, channels)
case tgTestAccount:
assert.Equal(t, []config.ChannelInfo{{
Name: "--333333333333",
Account: tgTestAccount,
Direction: "inout",
ID: "--333333333333telegram.zzz",
SameChannel: map[string]bool{"bridge3": false},
Options: config.ChannelOptions{Key: ""},
}}, channels)
} }
} }
case "announcements": case "announcements":
@ -348,42 +273,12 @@ func TestGetDestChannelAdvanced(t *testing.T) {
} }
hits[gw.Name]++ hits[gw.Name]++
switch br.Account { switch br.Account {
case ircTestAccount: case "irc.zzz":
assert.Len(t, channels, 2) assert.Equal(t, []config.ChannelInfo{{Name: "#main", Account: "irc.zzz", Direction: "out", ID: "#mainirc.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}, {Name: "#main-help", Account: "irc.zzz", Direction: "out", ID: "#main-helpirc.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
assert.Contains(t, channels, config.ChannelInfo{ case "slack.zzz":
Name: "#main", assert.Equal(t, []config.ChannelInfo{{Name: "general", Account: "slack.zzz", Direction: "out", ID: "generalslack.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
Account: ircTestAccount, case "telegram.zzz":
Direction: "out", assert.Equal(t, []config.ChannelInfo{{Name: "--333333333333", Account: "telegram.zzz", Direction: "out", ID: "--333333333333telegram.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels)
ID: "#mainirc.zzz",
SameChannel: map[string]bool{"announcements": false},
Options: config.ChannelOptions{Key: ""},
})
assert.Contains(t, channels, config.ChannelInfo{
Name: "#main-help",
Account: ircTestAccount,
Direction: "out",
ID: "#main-helpirc.zzz",
SameChannel: map[string]bool{"announcements": false},
Options: config.ChannelOptions{Key: ""},
})
case slackTestAccount:
assert.Equal(t, []config.ChannelInfo{{
Name: "general",
Account: slackTestAccount,
Direction: "out",
ID: "generalslack.zzz",
SameChannel: map[string]bool{"announcements": false},
Options: config.ChannelOptions{Key: ""},
}}, channels)
case tgTestAccount:
assert.Equal(t, []config.ChannelInfo{{
Name: "--333333333333",
Account: tgTestAccount,
Direction: "out",
ID: "--333333333333telegram.zzz",
SameChannel: map[string]bool{"announcements": false},
Options: config.ChannelOptions{Key: ""},
}}, channels)
} }
} }
} }
@ -391,139 +286,3 @@ func TestGetDestChannelAdvanced(t *testing.T) {
} }
assert.Equal(t, map[string]int{"bridge3": 4, "bridge": 9, "announcements": 3, "bridge2": 4}, hits) 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
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,21 +1,21 @@
package samechannel package samechannelgateway
import ( import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
) )
type SameChannelGateway struct { type SameChannelGateway struct {
config.Config *config.Config
} }
func New(cfg config.Config) *SameChannelGateway { func New(cfg *config.Config) *SameChannelGateway {
return &SameChannelGateway{Config: cfg} return &SameChannelGateway{Config: cfg}
} }
func (sgw *SameChannelGateway) GetConfig() []config.Gateway { func (sgw *SameChannelGateway) GetConfig() []config.Gateway {
var gwconfigs []config.Gateway var gwconfigs []config.Gateway
cfg := sgw.Config cfg := sgw.Config
for _, gw := range cfg.BridgeValues().SameChannelGateway { for _, gw := range cfg.SameChannelGateway {
gwconfig := config.Gateway{Name: gw.Name, Enable: gw.Enable} gwconfig := config.Gateway{Name: gw.Name, Enable: gw.Enable}
for _, account := range gw.Accounts { for _, account := range gw.Accounts {
for _, channel := range gw.Channels { for _, channel := range gw.Channels {

View File

@ -1,15 +1,15 @@
package samechannel package samechannelgateway
import ( import (
"io/ioutil" "fmt"
"testing"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus" "github.com/BurntSushi/toml"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing"
) )
const testConfig = ` var testconfig = `
[mattermost.test] [mattermost.test]
[slack.test] [slack.test]
@ -20,58 +20,12 @@ const testConfig = `
channels = [ "testing","testing2","testing10"] channels = [ "testing","testing2","testing10"]
` `
var (
expectedConfig = config.Gateway{
Name: "blah",
Enable: true,
In: []config.Bridge(nil),
Out: []config.Bridge(nil),
InOut: []config.Bridge{
{
Account: "mattermost.test",
Channel: "testing",
Options: config.ChannelOptions{Key: ""},
SameChannel: true,
},
{
Account: "mattermost.test",
Channel: "testing2",
Options: config.ChannelOptions{Key: ""},
SameChannel: true,
},
{
Account: "mattermost.test",
Channel: "testing10",
Options: config.ChannelOptions{Key: ""},
SameChannel: true,
},
{
Account: "slack.test",
Channel: "testing",
Options: config.ChannelOptions{Key: ""},
SameChannel: true,
},
{
Account: "slack.test",
Channel: "testing2",
Options: config.ChannelOptions{Key: ""},
SameChannel: true,
},
{
Account: "slack.test",
Channel: "testing10",
Options: config.ChannelOptions{Key: ""},
SameChannel: true,
},
},
}
)
func TestGetConfig(t *testing.T) { func TestGetConfig(t *testing.T) {
logger := logrus.New() var cfg *config.Config
logger.SetOutput(ioutil.Discard) if _, err := toml.Decode(testconfig, &cfg); err != nil {
cfg := config.NewConfigFromString(logger, []byte(testConfig)) fmt.Println(err)
}
sgw := New(cfg) sgw := New(cfg)
configs := sgw.GetConfig() configs := sgw.GetConfig()
assert.Equal(t, []config.Gateway{expectedConfig}, configs) assert.Equal(t, []config.Gateway{{Name: "blah", Enable: true, In: []config.Bridge(nil), Out: []config.Bridge(nil), InOut: []config.Bridge{{Account: "mattermost.test", Channel: "testing", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "mattermost.test", Channel: "testing2", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "mattermost.test", Channel: "testing10", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "slack.test", Channel: "testing", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "slack.test", Channel: "testing2", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "slack.test", Channel: "testing10", Options: config.ChannelOptions{Key: ""}, SameChannel: true}}}}, configs)
} }

74
go.mod
View File

@ -1,74 +0,0 @@
module github.com/42wim/matterbridge
require (
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 // indirect
github.com/Jeffail/gabs v1.1.1 // indirect
github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329
github.com/bwmarrin/discordgo v0.19.0
github.com/d5/tengo v1.9.2
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 v4.6.5-0.20181225215658-ec221ba9ea45+incompatible
github.com/google/gops v0.3.5
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect
github.com/gorilla/schema v1.0.2
github.com/gorilla/websocket v1.4.0
github.com/hashicorp/golang-lru v0.5.0
github.com/hpcloud/tail v1.0.0 // indirect
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-20170510131534-ae77be60afb1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/labstack/echo/v4 v4.0.0
github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d
github.com/matterbridge/go-whatsapp v0.0.1-0.20190301204034-f2f1b29d441b
github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91
github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea
github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18
github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61
github.com/matterbridge/mautrix-whatsapp v0.0.0-20190301210046-3539cf52ed6e
github.com/mattermost/mattermost-server v5.5.0+incompatible
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/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.5.0
github.com/onsi/ginkgo v1.6.0 // indirect
github.com/onsi/gomega v1.4.1 // indirect
github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83
github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320
github.com/pkg/errors v0.8.0 // indirect
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/ssh-chat v0.0.0-20190125184227-81d7e1686296
github.com/sirupsen/logrus v1.3.0
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
github.com/spf13/viper v1.3.1
github.com/stretchr/testify v1.3.0
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a // indirect
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179 // indirect
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2 // indirect
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe // indirect
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 // indirect
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.9.1 // indirect
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)

232
go.sum
View File

@ -1,232 +0,0 @@
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 h1:IZtuWGfzQnKnCSu+vl8WGLhpVQ5Uvy3rlSwqXSg+sQg=
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557/go.mod h1:jL0YSXMs/txjtGJ4PWrmETOk6KUHMDPMshgQZlTeB3Y=
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII=
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk=
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 h1:v/zr4ns/4sSahF9KBm4Uc933bLsEEv7LuT63CJ019yo=
github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E=
github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 h1:xZBoq249G9MSt+XuY7sVQzcfONJ6IQuwpCK+KAaOpnY=
github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg=
github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58/go.mod h1:YNfsMyWSs+h+PaYkxGeMVmVCX75Zj/pqdjbu12ciCYE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY=
github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/d5/tengo v1.9.2 h1:UE/X8PYl7bLS4Ww2zGeh91nq5PTnkhe8ncgNeA5PK7k=
github.com/d5/tengo v1.9.2/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-20180902042739-76ee6ab99bec h1:JEUiu7P9smN7zgX87a2zVnnbPPickIM9Gf9OIhsIgWQ=
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec/go.mod h1:UGa5M2Sz/Uh13AMse4+RELKCDw7kqgqlTjeGae+7vUY=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible h1:i64CCJcSqkRIkm5OSdZQjZq84/gJsk2zNwHWIRYWlKE=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gops v0.3.5 h1:SIWvPLiYvy5vMwjxB3rVFTE4QBhUFj2KKWr3Xm7CKhw=
github.com/google/gops v0.3.5/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0=
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 h1:4EZlYQIiyecYJlUbVkFXCXHz1QPhVXcHnQKAzBTPfQo=
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4/go.mod h1:lEO7XoHJ/xNRBCxrn4h/CEB67h0kW1B0t4ooP2yrjUA=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f h1:FDM3EtwZLyhW48YRiyqjivNlNZjAObv4xt4NnJaU+NQ=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA=
github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v1.3.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
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/v4 v4.0.0 h1:q1GH+caIXPP7H2StPIdzy/ez9CO0EepqYeUg6vi9SWM=
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398 h1:a40kRmhA1p2XFJ6gqXfCExSyuDDCp/U9LA8ZY27u2Lk=
github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398/go.mod h1:7cRs1SIBfKQ7e3Tam6GKTILSNHzR862JD0JpINaZoJk=
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU=
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0=
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns=
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d h1:F+Sr+C0ojSlYQ37BLylQtSFmyQULe3jbAygcyXQ9mVs=
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d/go.mod h1:c6MxwqHD+0HvtAJjsHMIdPCiAwGiQwPRPTp69ACMg8A=
github.com/matterbridge/go-whatsapp v0.0.1-0.20190301204034-f2f1b29d441b h1:cO6Z+yj4Ivq/ay/IxSrV90oSIW/SSXWLa+XHsiLKMrw=
github.com/matterbridge/go-whatsapp v0.0.1-0.20190301204034-f2f1b29d441b/go.mod h1:dW19fYkkdUZsBAx7zv9fDh0n6NRqYIaKwB2JEBw8d0U=
github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91 h1:KzDEcy8eDbTx881giW8a6llsAck3e2bJvMyKvh1IK+k=
github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91/go.mod h1:ECDRehsR9TYTKCAsRS8/wLeOk6UUqDydw47ln7wG41Q=
github.com/matterbridge/gomatrix v0.0.0-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/matterbridge/mautrix-whatsapp v0.0.0-20190301210046-3539cf52ed6e h1:1NqciL8sz+0UYeFrd/UQlL8tJPhFxOBmg+a94DN2sJU=
github.com/matterbridge/mautrix-whatsapp v0.0.0-20190301210046-3539cf52ed6e/go.mod h1:DrIFGcFumRlEW5k3PJjWGKPd4+w37d3SwOxlh1ZAL+4=
github.com/mattermost/mattermost-server v5.5.0+incompatible h1:0wcLGgYtd+YImtLDPf2AOfpBHxbU4suATx+6XKw1XbU=
github.com/mattermost/mattermost-server v5.5.0+incompatible/go.mod h1:5L6MjAec+XXQwMIt791Ganu45GKsSiM+I0tLR9wUj8Y=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 h1:oKIteTqeSpenyTrOVj5zkiyCaflLa8B+CD0324otT+o=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff h1:HLGD5/9UxxfEuO9DtP8gnTmNtMxbPyhYltfxsITel8g=
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E=
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 h1:mp6tU1r0xLostUGLkTspf/9/AiHuVD7ptyXhySkDEsE=
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9/go.mod h1:A5SRAcpTemjGgIuBq6Kic2yHcoeUFWUinOAlMP/i9xo=
github.com/nicksnyder/go-i18n v1.4.0 h1:AgLl+Yq7kg5OYlzCgu9cKTZOyI4tD/NgukKqLqC8E+I=
github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0=
github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
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/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 v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320 h1:YxcQy/DV+48NGv1lxx1vsWBzs6W1f1ogubkuCozxpX0=
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320/go.mod h1:G7LufuPajuIvdt9OitkNt2qh0mmvD4bfRgRM7bhDIOA=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 h1:Lx3BlDGFElJt4u/zKc9A3BuGYbQAGlEFyPuUA3jeMD0=
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI=
github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296 h1:8RLq547MSVc6vhOuCl4Ca0TsAQknj6NX6ZLSZ3+xmio=
github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296/go.mod h1:1GLXsL4esywkpNId3v4QWuMf3THtWGitWvtQ/L3aSA4=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 h1:lXQ+j+KwZcbwrbgU0Rp4Eglg3EJLHbuZU3BbOqAGBmg=
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38=
github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/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/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447 h1:CHgPZh8bFkZmislPrr/0gd7MciDAX+JJB70A2/5Lvmo=
github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU=
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a h1:Ax7kdHNICZiIeFpmevmaEWb0Ae3BUj3zCTKhZHZ+zd0=
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a/go.mod h1:JT4uoTz0tfPoyVH88GZoWDNm5NHJI2VbUW+eyPClueI=
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179 h1:rbON2KwBnWuFMlSHM8LELLlwroDRZw6xv0e6il6e5dk=
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8=
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f h1:jwXy/CsM4xS2aoiF2fHAlukmInWhd2TlWB+HDCyvzKc=
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f/go.mod h1:SIHlEr9462fpIfTrVWf3GqQDxnA65Vm3BMMsUtuA6W0=
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2 h1:wD/sPUgx2QJFPTyXZpJnLaROolfeKuruh06U4pRV0WY=
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2/go.mod h1:wQk4rLkWrdOPjUAtqJRJ10hIlseLSVYWP95PLrjDF9s=
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe h1:5kUPFAF52umOUPH12MuNUmyVTseJRNBftDl/KfsvX3I=
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe/go.mod h1:P9LSM1KVzrIstFgUaveuwiAm8PK5VTB3yJEU8kqlbrU=
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 h1:uPZaMiz6Sz0PZs3IZJWpU5qHKGNy///1pacZC9txiUI=
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638/go.mod h1:EGRJaqe2eO9XGmFtQCvV3Lm9NLico3UhFwUpCG/+mVU=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f h1:qWFY9ZxP3tfI37wYIs/MnIAqK0vlXp1xnYEa5HxFSSY=
golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9 h1:+vH8qNweCrORN49012OX3h0oWEXO3p+rRnpAGQinddk=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222171317-cd391775e71e h1:oF7qaQxUH6KzFdKN4ww7NpPdo53SZi4UlcksLrb2y/o=
golang.org/x/sys v0.0.0-20190222171317-cd391775e71e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/maulogger/v2 v2.0.0/go.mod h1:Hbbkq3NV6jvJodByZu1mgEF3fpT7Kz9z0MjEZ3/BusI=
maunium.net/go/mautrix v0.1.0-alpha.3/go.mod h1:GTVu6WDHR+98DKOrYetWsXorvUeKQV3jsSWO6ScbuFI=
maunium.net/go/mautrix-appservice v0.1.0-alpha.3/go.mod h1:wOnWOIuprYad7ly12rHIo3JLCPh4jwvx1prVrAB9RhM=

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

View File

@ -3,83 +3,54 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"os"
"strings"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway" "github.com/42wim/matterbridge/gateway"
"github.com/42wim/matterbridge/gateway/bridgemap" log "github.com/Sirupsen/logrus"
"github.com/google/gops/agent" "github.com/google/gops/agent"
prefixed "github.com/matterbridge/logrus-prefixed-formatter" "os"
"github.com/sirupsen/logrus" "strings"
) )
var ( var (
version = "1.14.0-rc2" version = "1.6.1"
githash string 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 init() {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
}
func main() { func 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() flag.Parse()
if *flagGops {
agent.Listen(&agent.Options{})
defer agent.Close()
}
if *flagVersion { if *flagVersion {
fmt.Printf("version: %s %s\n", version, githash) fmt.Printf("version: %s %s\n", version, githash)
return return
} }
if *flagDebug || os.Getenv("DEBUG") == "1" {
rootLogger := setupLogger() log.Info("Enabling debug")
logger := rootLogger.WithFields(logrus.Fields{"prefix": "main"}) log.SetLevel(log.DebugLevel)
if *flagGops {
if err := agent.Listen(agent.Options{}); err != nil {
logger.Errorf("Failed to start gops agent: %#v", err)
} else {
defer agent.Close()
}
} }
log.Printf("Running version %s %s", version, githash)
logger.Printf("Running version %s %s", version, githash)
if strings.Contains(version, "-dev") { if strings.Contains(version, "-dev") {
logger.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.") log.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
} }
cfg := config.NewConfig(*flagConfig)
cfg := config.NewConfig(rootLogger, *flagConfig) r, err := gateway.NewRouter(cfg)
cfg.BridgeValues().General.Debug = *flagDebug
r, err := gateway.NewRouter(rootLogger, cfg, bridgemap.FullMap)
if err != nil { if err != nil {
logger.Fatalf("Starting gateway failed: %s", err) log.Fatalf("Starting gateway failed: %s", err)
} }
if err = r.Start(); err != nil { err = r.Start()
logger.Fatalf("Starting gateway failed: %s", err) if err != nil {
log.Fatalf("Starting gateway failed: %s", err)
} }
logger.Printf("Gateway(s) started succesfully. Now relaying messages") log.Printf("Gateway(s) started succesfully. Now relaying messages")
select {} 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
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,26 +1,30 @@
package matterclient package matterclient
import ( import (
"crypto/md5"
"crypto/tls"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strings" "strings"
"sync" "sync"
"time" "time"
log "github.com/Sirupsen/logrus"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
lru "github.com/hashicorp/golang-lru" "github.com/hashicorp/golang-lru"
"github.com/jpillora/backoff" "github.com/jpillora/backoff"
prefixed "github.com/matterbridge/logrus-prefixed-formatter" "github.com/mattermost/platform/model"
"github.com/mattermost/mattermost-server/model"
"github.com/sirupsen/logrus"
) )
type Credentials struct { type Credentials struct {
Login string Login string
Team string Team string
Pass string Pass string
Token string
CookieToken bool
Server string Server string
NoTLS bool NoTLS bool
SkipTLSVerify bool SkipTLSVerify bool
@ -37,7 +41,6 @@ type Message struct {
UserID string UserID string
} }
//nolint:golint
type Team struct { type Team struct {
Team *model.Team Team *model.Team
Id string Id string
@ -49,13 +52,13 @@ type Team struct {
type MMClient struct { type MMClient struct {
sync.RWMutex sync.RWMutex
*Credentials *Credentials
Team *Team Team *Team
OtherTeams []*Team OtherTeams []*Team
Client *model.Client4 Client *model.Client4
User *model.User User *model.User
Users map[string]*model.User Users map[string]*model.User
MessageChan chan *Message MessageChan chan *Message
log *log.Entry
WsClient *websocket.Conn WsClient *websocket.Conn
WsQuit bool WsQuit bool
WsAway bool WsAway bool
@ -64,61 +67,27 @@ type MMClient struct {
WsPingChan chan *model.WebSocketResponse WsPingChan chan *model.WebSocketResponse
ServerVersion string ServerVersion string
OnWsConnect func() OnWsConnect func()
lruCache *lru.Cache
logger *logrus.Entry
rootLogger *logrus.Logger
lruCache *lru.Cache
} }
// New will instantiate a new Matterclient with the specified login details without connecting. func New(login, pass, team, server string) *MMClient {
func New(login string, pass string, team string, server string) *MMClient { cred := &Credentials{Login: login, Pass: pass, Team: team, Server: server}
rootLogger := logrus.New() mmclient := &MMClient{Credentials: cred, MessageChan: make(chan *Message, 100), Users: make(map[string]*model.User)}
rootLogger.SetFormatter(&prefixed.TextFormatter{ mmclient.log = log.WithFields(log.Fields{"module": "matterclient"})
PrefixPadding: 13, log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
DisableColors: true, mmclient.lruCache, _ = lru.New(500)
}) return mmclient
cred := &Credentials{
Login: login,
Pass: pass,
Team: team,
Server: server,
}
cache, _ := lru.New(500)
return &MMClient{
Credentials: cred,
MessageChan: make(chan *Message, 100),
Users: make(map[string]*model.User),
rootLogger: rootLogger,
lruCache: cache,
logger: rootLogger.WithFields(logrus.Fields{"prefix": "matterclient"}),
}
} }
// SetDebugLog activates debugging logging on all Matterclient log output.
func (m *MMClient) SetDebugLog() {
m.rootLogger.SetFormatter(&prefixed.TextFormatter{
PrefixPadding: 13,
DisableColors: true,
FullTimestamp: false,
ForceFormatting: true,
})
}
// SetLogLevel tries to parse the specified level and if successful sets
// the log level accordingly. Accepted levels are: 'debug', 'info', 'warn',
// 'error', 'fatal' and 'panic'.
func (m *MMClient) SetLogLevel(level string) { func (m *MMClient) SetLogLevel(level string) {
l, err := logrus.ParseLevel(level) l, err := log.ParseLevel(level)
if err != nil { if err != nil {
m.logger.Warnf("Failed to parse specified log-level '%s': %#v", level, err) log.SetLevel(log.InfoLevel)
} else { return
m.rootLogger.SetLevel(l)
} }
log.SetLevel(l)
} }
// Login tries to connect the client with the loging details with which it was initialized.
func (m *MMClient) Login() error { func (m *MMClient) Login() error {
// check if this is a first connect or a reconnection // check if this is a first connect or a reconnection
firstConnection := true firstConnection := true
@ -134,26 +103,89 @@ func (m *MMClient) Login() error {
Max: 5 * time.Minute, Max: 5 * time.Minute,
Jitter: true, Jitter: true,
} }
uriScheme := "https://"
// do initialization setup if m.NoTLS {
if err := m.initClient(firstConnection, b); err != nil { uriScheme = "http://"
return err
} }
// login to mattermost
m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, Proxy: http.ProxyFromEnvironment}
m.Client.HttpClient.Timeout = time.Second * 10
if err := m.doLogin(firstConnection, b); err != nil { for {
return err d := b.Duration()
// bogus call to get the serverversion
_, resp := m.Client.Logout()
if resp.Error != nil {
return fmt.Errorf("%#v", resp.Error.Error())
}
if firstConnection && !supportedVersion(resp.ServerVersion) {
return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion)
}
m.ServerVersion = resp.ServerVersion
if m.ServerVersion == "" {
m.log.Debugf("Server not up yet, reconnecting in %s", d)
time.Sleep(d)
} else {
m.log.Infof("Found version %s", m.ServerVersion)
break
}
} }
b.Reset()
if err := m.initUser(); err != nil { var resp *model.Response
//var myinfo *model.Result
var appErr *model.AppError
var logmsg = "trying login"
for {
m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.log.Debugf(logmsg + " with token")
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
if len(token) != 2 {
return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
}
m.Client.HttpClient.Jar = m.createCookieJar(token[1])
m.Client.AuthToken = token[1]
m.Client.AuthType = model.HEADER_BEARER
m.User, resp = m.Client.GetMe("")
if resp.Error != nil {
return resp.Error
}
if m.User == nil {
m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
return errors.New("invalid " + model.SESSION_COOKIE_TOKEN)
}
} else {
m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
}
appErr = resp.Error
if appErr != nil {
d := b.Duration()
m.log.Debug(appErr.DetailedError)
if firstConnection {
if appErr.Message == "" {
return errors.New(appErr.DetailedError)
}
return errors.New(appErr.Message)
}
m.log.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
time.Sleep(d)
logmsg = "retrying login"
continue
}
break
}
// reset timer
b.Reset()
err := m.initUser()
if err != nil {
return err return err
} }
if m.Team == nil { if m.Team == nil {
validTeamNames := make([]string, len(m.OtherTeams)) return errors.New("team not found")
for i, t := range m.OtherTeams {
validTeamNames[i] = t.Team.Name
}
return fmt.Errorf("Team '%s' not found in %v", m.Credentials.Team, validTeamNames)
} }
m.wsConnect() m.wsConnect()
@ -161,14 +193,52 @@ func (m *MMClient) Login() error {
return nil return nil
} }
// Logout disconnects the client from the chat server. 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.log.Debugf("WsClient: making connection: %s", wsurl)
for {
wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}}
var err error
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
if err != nil {
d := b.Duration()
m.log.Debugf("WSS: %s, reconnecting in %s", err, d)
time.Sleep(d)
continue
}
break
}
m.log.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) Logout() error { func (m *MMClient) Logout() error {
m.logger.Debugf("logout as %s (team: %s) on %s", m.Credentials.Login, m.Credentials.Team, m.Credentials.Server) m.log.Debugf("logout as %s (team: %s) on %s", m.Credentials.Login, m.Credentials.Team, m.Credentials.Server)
m.WsQuit = true m.WsQuit = true
m.WsClient.Close() m.WsClient.Close()
m.WsClient.UnderlyingConn().Close() m.WsClient.UnderlyingConn().Close()
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) { if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.logger.Debug("Not invalidating session in logout, credential is a token") m.log.Debug("Not invalidating session in logout, credential is a token")
return nil return nil
} }
_, resp := m.Client.Logout() _, resp := m.Client.Logout()
@ -178,16 +248,13 @@ func (m *MMClient) Logout() error {
return nil return nil
} }
// WsReceiver implements the core loop that manages the connection to the chat server. In
// case of a disconnect it will try to reconnect. A call to this method is blocking until
// the 'WsQuite' field of the MMClient object is set to 'true'.
func (m *MMClient) WsReceiver() { func (m *MMClient) WsReceiver() {
for { for {
var rawMsg json.RawMessage var rawMsg json.RawMessage
var err error var err error
if m.WsQuit { if m.WsQuit {
m.logger.Debug("exiting WsReceiver") m.log.Debug("exiting WsReceiver")
return return
} }
@ -197,14 +264,14 @@ func (m *MMClient) WsReceiver() {
} }
if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil { if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil {
m.logger.Error("error:", err) m.log.Error("error:", err)
// reconnect // reconnect
m.wsConnect() m.wsConnect()
} }
var event model.WebSocketEvent var event model.WebSocketEvent
if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() { if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
m.logger.Debugf("WsReceiver event: %#v", event) m.log.Debugf("WsReceiver event: %#v", event)
msg := &Message{Raw: &event, Team: m.Credentials.Team} msg := &Message{Raw: &event, Team: m.Credentials.Team}
m.parseMessage(msg) m.parseMessage(msg)
// check if we didn't empty the message // check if we didn't empty the message
@ -223,42 +290,539 @@ func (m *MMClient) WsReceiver() {
var response model.WebSocketResponse var response model.WebSocketResponse
if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() { if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
m.logger.Debugf("WsReceiver response: %#v", response) m.log.Debugf("WsReceiver response: %#v", response)
m.parseResponse(response) m.parseResponse(response)
continue
} }
} }
} }
// StatusLoop implements a ping-cycle that ensures that the connection to the chat servers func (m *MMClient) parseMessage(rmsg *Message) {
// remains alive. In case of a disconnect it will try to reconnect. A call to this method switch rmsg.Raw.Event {
// is blocking until the 'WsQuite' field of the MMClient object is set to 'true'. case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED:
m.parseActionPost(rmsg)
/*
case model.ACTION_USER_REMOVED:
m.handleWsActionUserRemoved(&rmsg)
case model.ACTION_USER_ADDED:
m.handleWsActionUserAdded(&rmsg)
*/
}
}
func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) {
if rmsg.Data != nil {
// ping reply
if rmsg.Data["text"].(string) == "pong" {
m.WsPingChan <- &rmsg
}
}
}
func (m *MMClient) parseActionPost(rmsg *Message) {
// 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.log.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.log.Infof("User %s is not known, ignoring message %s", 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) UpdateUsers() error {
mmusers, resp := m.Client.GetUsers(0, 50000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
m.Lock()
for _, user := range mmusers {
m.Users[user.Id] = user
}
m.Unlock()
return nil
}
func (m *MMClient) UpdateChannels() error {
mmchannels, resp := m.Client.GetChannelsForTeamForUser(m.Team.Id, m.User.Id, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
m.Lock()
m.Team.Channels = mmchannels
m.Unlock()
mmchannels, resp = m.Client.GetPublicChannelsForTeam(m.Team.Id, 0, 5000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
m.Lock()
m.Team.MoreChannels = mmchannels
m.Unlock()
return nil
}
func (m *MMClient) GetChannelName(channelId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if t == nil {
continue
}
if t.Channels != nil {
for _, channel := range t.Channels {
if channel.Id == channelId {
return channel.Name
}
}
}
if t.MoreChannels != nil {
for _, channel := range t.MoreChannels {
if channel.Id == channelId {
return channel.Name
}
}
}
}
return ""
}
func (m *MMClient) GetChannelId(name string, teamId string) string {
m.RLock()
defer m.RUnlock()
if teamId == "" {
teamId = m.Team.Id
}
for _, t := range m.OtherTeams {
if t.Id == teamId {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Name == name {
return channel.Id
}
}
}
}
return ""
}
func (m *MMClient) GetChannelTeamId(id string) string {
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) GetChannelHeader(channelId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == channelId {
return channel.Header
}
}
}
return ""
}
func (m *MMClient) PostMessage(channelId string, text string) (string, error) {
post := &model.Post{ChannelId: channelId, Message: text}
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, fileIds []string) (string, error) {
post := &model.Post{ChannelId: channelId, Message: text, FileIds: fileIds}
res, resp := m.Client.CreatePost(post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) EditMessage(postId string, text string) (string, error) {
post := &model.Post{Message: text}
res, resp := m.Client.UpdatePost(postId, post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) DeleteMessage(postId string) error {
_, resp := m.Client.DeletePost(postId)
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) JoinChannel(channelId string) error {
m.RLock()
defer m.RUnlock()
for _, c := range m.Team.Channels {
if c.Id == channelId {
m.log.Debug("Not joining ", channelId, " already joined.")
return nil
}
}
m.log.Debug("Joining ", channelId)
_, resp := m.Client.AddChannelMember(channelId, m.User.Id)
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList {
res, resp := m.Client.GetPostsSince(channelId, time)
if resp.Error != nil {
return nil
}
return res
}
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
}
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList {
res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "")
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) 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_V3+"/files/"+f+"/get")
continue
}
output = append(output, res)
}
return output
}
func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
channel := &model.Channel{Id: channelId, Header: header}
m.log.Debugf("updating channelheader %#v, %#v", channelId, header)
_, resp := m.Client.UpdateChannel(channel)
if resp.Error != nil {
log.Error(resp.Error)
}
}
func (m *MMClient) UpdateLastViewed(channelId string) {
m.log.Debugf("posting lastview %#v", channelId)
view := &model.ChannelView{ChannelId: channelId}
res, _ := m.Client.ViewChannel(m.User.Id, view)
if !res {
m.log.Errorf("ChannelView update for %s failed", channelId)
}
}
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 {
res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "")
if resp.Error != nil {
m.log.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) createCookieJar(token string) *cookiejar.Jar {
var cookies []*http.Cookie
jar, _ := cookiejar.New(nil)
firstCookie := &http.Cookie{
Name: "MMAUTHTOKEN",
Value: token,
Path: "/",
Domain: m.Credentials.Server,
}
cookies = append(cookies, firstCookie)
cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
jar.SetCookies(cookieURL, cookies)
return jar
}
// SendDirectMessage sends a direct message to specified user
func (m *MMClient) SendDirectMessage(toUserId string, msg string) {
m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
// create DM channel (only happens on first message)
_, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId)
if resp.Error != nil {
m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error)
return
}
channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
// update our channels
m.UpdateChannels()
// build & send the message
msg = strings.Replace(msg, "\r", "", -1)
post := &model.Post{ChannelId: m.GetChannelId(channelName, ""), Message: msg}
m.Client.CreatePost(post)
}
// GetTeamName returns the name of the specified teamId
func (m *MMClient) GetTeamName(teamId string) string {
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if t.Id == teamId {
return t.Team.Name
}
}
return ""
}
// GetChannels returns all channels we're members off
func (m *MMClient) GetChannels() []*model.Channel {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
// our primary team channels first
channels = append(channels, m.Team.Channels...)
for _, t := range m.OtherTeams {
if t.Id != m.Team.Id {
channels = append(channels, t.Channels...)
}
}
return channels
}
// GetMoreChannels returns existing channels where we're not a member off.
func (m *MMClient) GetMoreChannels() []*model.Channel {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.MoreChannels...)
}
return channels
}
// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId).
func (m *MMClient) GetTeamFromChannel(channelId string) string {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.Channels...)
if t.MoreChannels != nil {
channels = append(channels, t.MoreChannels...)
}
for _, c := range channels {
if c.Id == channelId {
return t.Id
}
}
}
return ""
}
func (m *MMClient) GetLastViewedAt(channelId string) int64 {
m.RLock()
defer m.RUnlock()
res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "")
if resp.Error != nil {
return model.GetMillis()
}
return res.LastViewedAt
}
func (m *MMClient) GetUsers() map[string]*model.User {
users := make(map[string]*model.User)
m.RLock()
defer m.RUnlock()
for k, v := range m.Users {
users[k] = v
}
return users
}
func (m *MMClient) GetUser(userId string) *model.User {
m.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 {
user := m.GetUser(userId)
if user != nil {
return user.Username
}
return ""
}
func (m *MMClient) GetStatus(userId string) string {
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 {
return m.Team.Id
}
func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) {
f, resp := m.Client.UploadFile(data, channelId, filename)
if resp.Error != nil {
return "", resp.Error
}
return f.FileInfos[0].Id, nil
}
func (m *MMClient) StatusLoop() { func (m *MMClient) StatusLoop() {
retries := 0 retries := 0
backoff := time.Second * 60 backoff := time.Second * 60
if m.OnWsConnect != nil { if m.OnWsConnect != nil {
m.OnWsConnect() m.OnWsConnect()
} }
m.logger.Debug("StatusLoop:", m.OnWsConnect != nil) m.log.Debug("StatusLoop:", m.OnWsConnect)
for { for {
if m.WsQuit { if m.WsQuit {
return return
} }
if m.WsConnected { if m.WsConnected {
if err := m.checkAlive(); err != nil { m.log.Debug("WS PING")
m.logger.Errorf("Connection is not alive: %#v", err) m.sendWSRequest("ping", nil)
}
select { select {
case <-m.WsPingChan: case <-m.WsPingChan:
m.logger.Debug("WS PONG received") m.log.Debug("WS PONG received")
backoff = time.Second * 60 backoff = time.Second * 60
case <-time.After(time.Second * 5): case <-time.After(time.Second * 5):
if retries > 3 { if retries > 3 {
m.logger.Debug("StatusLoop() timeout") m.log.Debug("StatusLoop() timeout")
m.Logout() m.Logout()
m.WsQuit = false m.WsQuit = false
err := m.Login() err := m.Login()
if err != nil { if err != nil {
m.logger.Errorf("Login failed: %#v", err) log.Errorf("Login failed: %#v", err)
break break
} }
if m.OnWsConnect != nil { if m.OnWsConnect != nil {
@ -274,3 +838,74 @@ func (m *MMClient) StatusLoop() {
time.Sleep(backoff) time.Sleep(backoff)
} }
} }
// 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.log.Debug("initUser(): loading all team data")
teams, resp := m.Client.GetTeamsForUser(m.User.Id, "")
if resp.Error != nil {
return resp.Error
}
for _, team := range teams {
mmusers, resp := m.Client.GetUsersInTeam(team.Id, 0, 50000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
usermap := make(map[string]*model.User)
for _, user := range mmusers {
usermap[user.Id] = user
}
t := &Team{Team: team, Users: usermap, Id: team.Id}
mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, "")
if resp.Error != nil {
return resp.Error
}
t.Channels = mmchannels
mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "")
if resp.Error != nil {
return resp.Error
}
t.MoreChannels = mmchannels
m.OtherTeams = append(m.OtherTeams, t)
if team.Name == m.Credentials.Team {
m.Team = t
m.log.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) sendWSRequest(action string, data map[string]interface{}) error {
req := &model.WebSocketRequest{}
req.Seq = m.WsSequence
req.Action = action
req.Data = data
m.WsSequence++
m.log.Debugf("sendWsRequest %#v", req)
m.WsClient.WriteJSON(req)
return nil
}
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.") {
return true
}
return false
}
func digestString(s string) string {
return fmt.Sprintf("%x", md5.Sum([]byte(s)))
}

View File

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

View File

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

View File

@ -6,15 +6,13 @@ import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gorilla/schema"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
"time" "time"
"github.com/gorilla/schema"
"github.com/nlopes/slack"
) )
// OMessage for mattermost incoming webhook. (send to mattermost) // OMessage for mattermost incoming webhook. (send to mattermost)
@ -24,7 +22,7 @@ type OMessage struct {
IconEmoji string `json:"icon_emoji,omitempty"` IconEmoji string `json:"icon_emoji,omitempty"`
UserName string `json:"username,omitempty"` UserName string `json:"username,omitempty"`
Text string `json:"text"` Text string `json:"text"`
Attachments []slack.Attachment `json:"attachments,omitempty"` Attachments interface{} `json:"attachments,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Props map[string]interface{} `json:"props"` Props map[string]interface{} `json:"props"`
} }
@ -41,9 +39,9 @@ type IMessage struct {
Timestamp string `schema:"timestamp"` Timestamp string `schema:"timestamp"`
UserID string `schema:"user_id"` UserID string `schema:"user_id"`
UserName string `schema:"user_name"` UserName string `schema:"user_name"`
PostId string `schema:"post_id"` //nolint:golint PostId string `schema:"post_id"`
RawText string `schema:"raw_text"` RawText string `schema:"raw_text"`
ServiceId string `schema:"service_id"` //nolint:golint ServiceId string `schema:"service_id"`
Text string `schema:"text"` Text string `schema:"text"`
TriggerWord string `schema:"trigger_word"` TriggerWord string `schema:"trigger_word"`
FileIDs string `schema:"file_ids"` FileIDs string `schema:"file_ids"`
@ -51,8 +49,7 @@ type IMessage struct {
// Client for Mattermost. // Client for Mattermost.
type Client struct { type Client struct {
// URL for incoming webhooks on mattermost. Url string // URL for incoming webhooks on mattermost.
Url string // nolint:golint
In chan IMessage In chan IMessage
Out chan OMessage Out chan OMessage
httpclient *http.Client httpclient *http.Client
@ -71,7 +68,7 @@ type Config struct {
func New(url string, config Config) *Client { func New(url string, config Config) *Client {
c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config} c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config}
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}, //nolint:gosec TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
} }
c.httpclient = &http.Client{Transport: tr} c.httpclient = &http.Client{Transport: tr}
if !c.DisableServer { if !c.DisableServer {

View File

@ -1,3 +0,0 @@
.idea
/test
app.yaml

View File

@ -1,154 +0,0 @@
# gitter
Gitter API in Go
https://developer.gitter.im
#### Install
`go get github.com/sromku/go-gitter`
- [Initialize](#initialize)
- [Users](#users)
- [Rooms](#rooms)
- [Messages](#messages)
- [Stream](#stream)
- [Faye (Experimental)](#faye-experimental)
- [Debug](#debug)
- [App Engine](#app-engine)
##### Initialize
``` Go
api := gitter.New("YOUR_ACCESS_TOKEN")
```
##### Users
- Get current user
``` Go
user, err := api.GetUser()
```
##### Rooms
- Get all rooms
``` Go
rooms, err := api.GetRooms()
```
- Get room by id
``` Go
room, err := api.GetRoom("roomID")
```
- Get rooms of some user
``` Go
rooms, err := api.GetRooms("userID")
```
- Join room
``` Go
room, err := api.JoinRoom("roomID", "userID")
```
- Leave room
``` Go
room, err := api.LeaveRoom("roomID", "userID")
```
- Get room id
``` Go
id, err := api.GetRoomId("room/uri")
```
- Search gitter rooms
``` Go
rooms, err := api.SearchRooms("search/string")
```
##### Messages
- Get messages of room
``` Go
messages, err := api.GetMessages("roomID", nil)
```
- Get one message
``` Go
message, err := api.GetMessage("roomID", "messageID")
```
- Send message
``` Go
err := api.SendMessage("roomID", "free chat text")
```
##### Stream
Create stream to the room and start listening to incoming messages
``` Go
stream := api.Stream(room.Id)
go api.Listen(stream)
for {
event := <-stream.Event
switch ev := event.Data.(type) {
case *gitter.MessageReceived:
fmt.Println(ev.Message.From.Username + ": " + ev.Message.Text)
case *gitter.GitterConnectionClosed:
// connection was closed
}
}
```
Close stream connection
``` Go
stream.Close()
```
##### Faye (Experimental)
``` Go
faye := api.Faye(room.ID)
go faye.Listen()
for {
event := <-faye.Event
switch ev := event.Data.(type) {
case *gitter.MessageReceived:
fmt.Println(ev.Message.From.Username + ": " + ev.Message.Text)
case *gitter.GitterConnectionClosed: //this one is never called in Faye
// connection was closed
}
}
```
##### Debug
You can print the internal errors by enabling debug to true
``` Go
api.SetDebug(true, nil)
```
You can also define your own `io.Writer` in case you want to persist the logs somewhere.
For example keeping the errors on file
``` Go
logFile, err := os.Create("gitter.log")
api.SetDebug(true, logFile)
```
##### App Engine
Initialize app engine client and continue as usual
``` Go
c := appengine.NewContext(r)
client := urlfetch.Client(c)
api := gitter.New("YOUR_ACCESS_TOKEN")
api.SetClient(client)
```
[Documentation](https://godoc.org/github.com/sromku/go-gitter)

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

@ -0,0 +1,27 @@
// Copyright (c) 2009 Thomas Jager. 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 Google Inc. 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
// OWNER 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.

578
vendor/github.com/42wim/go-ircevent/irc.go generated vendored Normal file
View File

@ -0,0 +1,578 @@
// Copyright 2009 Thomas Jager <mail@jager.no> All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
This package provides an event based IRC client library. It allows to
register callbacks for the events you need to handle. Its features
include handling standard CTCP, reconnecting on errors and detecting
stones servers.
Details of the IRC protocol can be found in the following RFCs:
https://tools.ietf.org/html/rfc1459
https://tools.ietf.org/html/rfc2810
https://tools.ietf.org/html/rfc2811
https://tools.ietf.org/html/rfc2812
https://tools.ietf.org/html/rfc2813
The details of the client-to-client protocol (CTCP) can be found here: http://www.irchelp.org/irchelp/rfc/ctcpspec.html
*/
package irc
import (
"bufio"
"bytes"
"crypto/tls"
"errors"
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
"time"
)
const (
VERSION = "go-ircevent v2.1"
)
var ErrDisconnected = errors.New("Disconnect Called")
// Read data from a connection. To be used as a goroutine.
func (irc *Connection) readLoop() {
defer irc.Done()
br := bufio.NewReaderSize(irc.socket, 512)
errChan := irc.ErrorChan()
for {
select {
case <-irc.end:
return
default:
// Set a read deadline based on the combined timeout and ping frequency
// We should ALWAYS have received a response from the server within the timeout
// after our own pings
if irc.socket != nil {
irc.socket.SetReadDeadline(time.Now().Add(irc.Timeout + irc.PingFreq))
}
msg, err := br.ReadString('\n')
// We got past our blocking read, so bin timeout
if irc.socket != nil {
var zero time.Time
irc.socket.SetReadDeadline(zero)
}
if err != nil {
errChan <- err
return
}
if irc.Debug {
irc.Log.Printf("<-- %s\n", strings.TrimSpace(msg))
}
irc.Lock()
irc.lastMessage = time.Now()
irc.Unlock()
event, err := parseToEvent(msg)
event.Connection = irc
if err == nil {
/* XXX: len(args) == 0: args should be empty */
irc.RunCallbacks(event)
}
}
}
}
// Unescape tag values as defined in the IRCv3.2 message tags spec
// http://ircv3.net/specs/core/message-tags-3.2.html
func unescapeTagValue(value string) string {
value = strings.Replace(value, "\\:", ";", -1)
value = strings.Replace(value, "\\s", " ", -1)
value = strings.Replace(value, "\\\\", "\\", -1)
value = strings.Replace(value, "\\r", "\r", -1)
value = strings.Replace(value, "\\n", "\n", -1)
return value
}
//Parse raw irc messages
func parseToEvent(msg string) (*Event, error) {
msg = strings.TrimSuffix(msg, "\n") //Remove \r\n
msg = strings.TrimSuffix(msg, "\r")
event := &Event{Raw: msg}
if len(msg) < 5 {
return nil, errors.New("Malformed msg from server")
}
if msg[0] == '@' {
// IRCv3 Message Tags
if i := strings.Index(msg, " "); i > -1 {
event.Tags = make(map[string]string)
tags := strings.Split(msg[1:i], ";")
for _, data := range tags {
parts := strings.SplitN(data, "=", 2)
if len(parts) == 1 {
event.Tags[parts[0]] = ""
} else {
event.Tags[parts[0]] = unescapeTagValue(parts[1])
}
}
msg = msg[i+1 : len(msg)]
} else {
return nil, errors.New("Malformed msg from server")
}
}
if msg[0] == ':' {
if i := strings.Index(msg, " "); i > -1 {
event.Source = msg[1:i]
msg = msg[i+1 : len(msg)]
} else {
return nil, errors.New("Malformed msg from server")
}
if i, j := strings.Index(event.Source, "!"), strings.Index(event.Source, "@"); i > -1 && j > -1 && i < j {
event.Nick = event.Source[0:i]
event.User = event.Source[i+1 : j]
event.Host = event.Source[j+1 : len(event.Source)]
}
}
split := strings.SplitN(msg, " :", 2)
args := strings.Split(split[0], " ")
event.Code = strings.ToUpper(args[0])
event.Arguments = args[1:]
if len(split) > 1 {
event.Arguments = append(event.Arguments, split[1])
}
return event, nil
}
// Loop to write to a connection. To be used as a goroutine.
func (irc *Connection) writeLoop() {
defer irc.Done()
errChan := irc.ErrorChan()
for {
select {
case <-irc.end:
return
case b, ok := <-irc.pwrite:
if !ok || b == "" || irc.socket == nil {
return
}
if irc.Debug {
irc.Log.Printf("--> %s\n", strings.TrimSpace(b))
}
// Set a write deadline based on the time out
irc.socket.SetWriteDeadline(time.Now().Add(irc.Timeout))
_, err := irc.socket.Write([]byte(b))
// Past blocking write, bin timeout
var zero time.Time
irc.socket.SetWriteDeadline(zero)
if err != nil {
errChan <- err
return
}
}
}
}
// Pings the server if we have not received any messages for 5 minutes
// to keep the connection alive. To be used as a goroutine.
func (irc *Connection) pingLoop() {
defer irc.Done()
ticker := time.NewTicker(1 * time.Minute) // Tick every minute for monitoring
ticker2 := time.NewTicker(irc.PingFreq) // Tick at the ping frequency.
for {
select {
case <-ticker.C:
//Ping if we haven't received anything from the server within the keep alive period
if time.Since(irc.lastMessage) >= irc.KeepAlive {
irc.SendRawf("PING %d", time.Now().UnixNano())
}
case <-ticker2.C:
//Ping at the ping frequency
irc.SendRawf("PING %d", time.Now().UnixNano())
//Try to recapture nickname if it's not as configured.
irc.Lock()
if irc.nick != irc.nickcurrent {
irc.nickcurrent = irc.nick
irc.SendRawf("NICK %s", irc.nick)
}
irc.Unlock()
case <-irc.end:
ticker.Stop()
ticker2.Stop()
return
}
}
}
func (irc *Connection) isQuitting() bool {
irc.Lock()
defer irc.Unlock()
return irc.quit
}
// Main loop to control the connection.
func (irc *Connection) Loop() {
errChan := irc.ErrorChan()
connTime := time.Now()
for !irc.isQuitting() {
err := <-errChan
close(irc.end)
irc.Wait()
for !irc.isQuitting() {
irc.Log.Printf("Error, disconnected: %s\n", err)
if time.Now().Sub(connTime) < time.Second*5 {
irc.Log.Println("Rreconnecting too fast, sleeping 60 seconds")
time.Sleep(60 * time.Second)
}
if err = irc.Reconnect(); err != nil {
irc.Log.Printf("Error while reconnecting: %s\n", err)
time.Sleep(60 * time.Second)
} else {
errChan = irc.ErrorChan()
break
}
}
connTime = time.Now()
}
}
// Quit the current connection and disconnect from the server
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.6
func (irc *Connection) Quit() {
quit := "QUIT"
if irc.QuitMessage != "" {
quit = fmt.Sprintf("QUIT :%s", irc.QuitMessage)
}
irc.SendRaw(quit)
irc.Lock()
irc.stopped = true
irc.quit = true
irc.Unlock()
}
// Use the connection to join a given channel.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.1
func (irc *Connection) Join(channel string) {
irc.pwrite <- fmt.Sprintf("JOIN %s\r\n", channel)
}
// Leave a given channel.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.2
func (irc *Connection) Part(channel string) {
irc.pwrite <- fmt.Sprintf("PART %s\r\n", channel)
}
// Send a notification to a nickname. This is similar to Privmsg but must not receive replies.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2
func (irc *Connection) Notice(target, message string) {
irc.pwrite <- fmt.Sprintf("NOTICE %s :%s\r\n", target, message)
}
// Send a formated notification to a nickname.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2
func (irc *Connection) Noticef(target, format string, a ...interface{}) {
irc.Notice(target, fmt.Sprintf(format, a...))
}
// Send (action) message to a target (channel or nickname).
// No clear RFC on this one...
func (irc *Connection) Action(target, message string) {
irc.pwrite <- fmt.Sprintf("PRIVMSG %s :\001ACTION %s\001\r\n", target, message)
}
// Send formatted (action) message to a target (channel or nickname).
func (irc *Connection) Actionf(target, format string, a ...interface{}) {
irc.Action(target, fmt.Sprintf(format, a...))
}
// Send (private) message to a target (channel or nickname).
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.1
func (irc *Connection) Privmsg(target, message string) {
irc.pwrite <- fmt.Sprintf("PRIVMSG %s :%s\r\n", target, message)
}
// Send formated string to specified target (channel or nickname).
func (irc *Connection) Privmsgf(target, format string, a ...interface{}) {
irc.Privmsg(target, fmt.Sprintf(format, a...))
}
// Kick <user> from <channel> with <msg>. For no message, pass empty string ("")
func (irc *Connection) Kick(user, channel, msg string) {
var cmd bytes.Buffer
cmd.WriteString(fmt.Sprintf("KICK %s %s", channel, user))
if msg != "" {
cmd.WriteString(fmt.Sprintf(" :%s", msg))
}
cmd.WriteString("\r\n")
irc.pwrite <- cmd.String()
}
// Kick all <users> from <channel> with <msg>. For no message, pass
// empty string ("")
func (irc *Connection) MultiKick(users []string, channel string, msg string) {
var cmd bytes.Buffer
cmd.WriteString(fmt.Sprintf("KICK %s %s", channel, strings.Join(users, ",")))
if msg != "" {
cmd.WriteString(fmt.Sprintf(" :%s", msg))
}
cmd.WriteString("\r\n")
irc.pwrite <- cmd.String()
}
// Send raw string.
func (irc *Connection) SendRaw(message string) {
irc.pwrite <- message + "\r\n"
}
// Send raw formated string.
func (irc *Connection) SendRawf(format string, a ...interface{}) {
irc.SendRaw(fmt.Sprintf(format, a...))
}
// Set (new) nickname.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.2
func (irc *Connection) Nick(n string) {
irc.nick = n
irc.SendRawf("NICK %s", n)
}
// Determine nick currently used with the connection.
func (irc *Connection) GetNick() string {
return irc.nickcurrent
}
// Query information about a particular nickname.
// RFC 1459: https://tools.ietf.org/html/rfc1459#section-4.5.2
func (irc *Connection) Whois(nick string) {
irc.SendRawf("WHOIS %s", nick)
}
// Query information about a given nickname in the server.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.5.1
func (irc *Connection) Who(nick string) {
irc.SendRawf("WHO %s", nick)
}
// Set different modes for a target (channel or nickname).
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.3
func (irc *Connection) Mode(target string, modestring ...string) {
if len(modestring) > 0 {
mode := strings.Join(modestring, " ")
irc.SendRawf("MODE %s %s", target, mode)
return
}
irc.SendRawf("MODE %s", target)
}
func (irc *Connection) ErrorChan() chan error {
return irc.Error
}
// Returns true if the connection is connected to an IRC server.
func (irc *Connection) Connected() bool {
return !irc.stopped
}
// A disconnect sends all buffered messages (if possible),
// stops all goroutines and then closes the socket.
func (irc *Connection) Disconnect() {
if irc.socket != nil {
irc.socket.Close()
}
irc.ErrorChan() <- ErrDisconnected
}
// Reconnect to a server using the current connection.
func (irc *Connection) Reconnect() error {
irc.end = make(chan struct{})
return irc.Connect(irc.Server)
}
// Connect to a given server using the current connection configuration.
// This function also takes care of identification if a password is provided.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1
func (irc *Connection) Connect(server string) error {
irc.Server = server
// mark Server as stopped since there can be an error during connect
irc.stopped = true
// make sure everything is ready for connection
if len(irc.Server) == 0 {
return errors.New("empty 'server'")
}
if strings.Count(irc.Server, ":") != 1 {
return errors.New("wrong number of ':' in address")
}
if strings.Index(irc.Server, ":") == 0 {
return errors.New("hostname is missing")
}
if strings.Index(irc.Server, ":") == len(irc.Server)-1 {
return errors.New("port missing")
}
// check for valid range
ports := strings.Split(irc.Server, ":")[1]
port, err := strconv.Atoi(ports)
if err != nil {
return errors.New("extracting port failed")
}
if !((port >= 0) && (port <= 65535)) {
return errors.New("port number outside valid range")
}
if irc.Log == nil {
return errors.New("'Log' points to nil")
}
if len(irc.nick) == 0 {
return errors.New("empty 'nick'")
}
if len(irc.user) == 0 {
return errors.New("empty 'user'")
}
if irc.UseTLS {
dialer := &net.Dialer{Timeout: irc.Timeout}
irc.socket, err = tls.DialWithDialer(dialer, "tcp", irc.Server, irc.TLSConfig)
} else {
irc.socket, err = net.DialTimeout("tcp", irc.Server, irc.Timeout)
}
if err != nil {
return err
}
irc.stopped = false
irc.Log.Printf("Connected to %s (%s)\n", irc.Server, irc.socket.RemoteAddr())
irc.pwrite = make(chan string, 10)
irc.Error = make(chan error, 2)
irc.Add(3)
go irc.readLoop()
go irc.writeLoop()
go irc.pingLoop()
if len(irc.Password) > 0 {
irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password)
}
err = irc.negotiateCaps()
if err != nil {
return err
}
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user)
return nil
}
// Negotiate IRCv3 capabilities
func (irc *Connection) negotiateCaps() error {
saslResChan := make(chan *SASLResult)
if irc.UseSASL {
irc.RequestCaps = append(irc.RequestCaps, "sasl")
irc.setupSASLCallbacks(saslResChan)
}
if len(irc.RequestCaps) == 0 {
return nil
}
cap_chan := make(chan bool, len(irc.RequestCaps))
irc.AddCallback("CAP", func(e *Event) {
if len(e.Arguments) != 3 {
return
}
command := e.Arguments[1]
if command == "LS" {
missing_caps := len(irc.RequestCaps)
for _, cap_name := range strings.Split(e.Arguments[2], " ") {
for _, req_cap := range irc.RequestCaps {
if cap_name == req_cap {
irc.pwrite <- fmt.Sprintf("CAP REQ :%s\r\n", cap_name)
missing_caps--
}
}
}
for i := 0; i < missing_caps; i++ {
cap_chan <- true
}
} else if command == "ACK" || command == "NAK" {
for _, cap_name := range strings.Split(strings.TrimSpace(e.Arguments[2]), " ") {
if cap_name == "" {
continue
}
if command == "ACK" {
irc.AcknowledgedCaps = append(irc.AcknowledgedCaps, cap_name)
}
cap_chan <- true
}
}
})
irc.pwrite <- "CAP LS\r\n"
if irc.UseSASL {
select {
case res := <-saslResChan:
if res.Failed {
close(saslResChan)
return res.Err
}
case <-time.After(time.Second * 15):
close(saslResChan)
return errors.New("SASL setup timed out. This shouldn't happen.")
}
}
// Wait for all capabilities to be ACKed or NAKed before ending negotiation
for i := 0; i < len(irc.RequestCaps); i++ {
<-cap_chan
}
irc.pwrite <- fmt.Sprintf("CAP END\r\n")
return nil
}
// Create a connection with the (publicly visible) nickname and username.
// The nickname is later used to address the user. Returns nil if nick
// or user are empty.
func IRC(nick, user string) *Connection {
// catch invalid values
if len(nick) == 0 {
return nil
}
if len(user) == 0 {
return nil
}
irc := &Connection{
nick: nick,
nickcurrent: nick,
user: user,
Log: log.New(os.Stdout, "", log.LstdFlags),
end: make(chan struct{}),
Version: VERSION,
KeepAlive: 4 * time.Minute,
Timeout: 1 * time.Minute,
PingFreq: 15 * time.Minute,
SASLMech: "PLAIN",
QuitMessage: "",
}
irc.setupCallbacks()
return irc
}

222
vendor/github.com/42wim/go-ircevent/irc_callback.go generated vendored Normal file
View File

@ -0,0 +1,222 @@
package irc
import (
"strconv"
"strings"
"time"
)
// Register a callback to a connection and event code. A callback is a function
// which takes only an Event pointer as parameter. Valid event codes are all
// IRC/CTCP commands and error/response codes. This function returns the ID of
// the registered callback for later management.
func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) int {
eventcode = strings.ToUpper(eventcode)
id := 0
if _, ok := irc.events[eventcode]; !ok {
irc.events[eventcode] = make(map[int]func(*Event))
id = 0
} else {
id = len(irc.events[eventcode])
}
irc.events[eventcode][id] = callback
return id
}
// Remove callback i (ID) from the given event code. This functions returns
// true upon success, false if any error occurs.
func (irc *Connection) RemoveCallback(eventcode string, i int) bool {
eventcode = strings.ToUpper(eventcode)
if event, ok := irc.events[eventcode]; ok {
if _, ok := event[i]; ok {
delete(irc.events[eventcode], i)
return true
}
irc.Log.Printf("Event found, but no callback found at id %d\n", i)
return false
}
irc.Log.Println("Event not found")
return false
}
// Remove all callbacks from a given event code. It returns true
// if given event code is found and cleared.
func (irc *Connection) ClearCallback(eventcode string) bool {
eventcode = strings.ToUpper(eventcode)
if _, ok := irc.events[eventcode]; ok {
irc.events[eventcode] = make(map[int]func(*Event))
return true
}
irc.Log.Println("Event not found")
return false
}
// Replace callback i (ID) associated with a given event code with a new callback function.
func (irc *Connection) ReplaceCallback(eventcode string, i int, callback func(*Event)) {
eventcode = strings.ToUpper(eventcode)
if event, ok := irc.events[eventcode]; ok {
if _, ok := event[i]; ok {
event[i] = callback
return
}
irc.Log.Printf("Event found, but no callback found at id %d\n", i)
}
irc.Log.Printf("Event not found. Use AddCallBack\n")
}
// Execute all callbacks associated with a given event.
func (irc *Connection) RunCallbacks(event *Event) {
msg := event.Message()
if event.Code == "PRIVMSG" && len(msg) > 2 && msg[0] == '\x01' {
event.Code = "CTCP" //Unknown CTCP
if i := strings.LastIndex(msg, "\x01"); i > 0 {
msg = msg[1:i]
} else {
irc.Log.Printf("Invalid CTCP Message: %s\n", strconv.Quote(msg))
return
}
if msg == "VERSION" {
event.Code = "CTCP_VERSION"
} else if msg == "TIME" {
event.Code = "CTCP_TIME"
} else if strings.HasPrefix(msg, "PING") {
event.Code = "CTCP_PING"
} else if msg == "USERINFO" {
event.Code = "CTCP_USERINFO"
} else if msg == "CLIENTINFO" {
event.Code = "CTCP_CLIENTINFO"
} else if strings.HasPrefix(msg, "ACTION") {
event.Code = "CTCP_ACTION"
if len(msg) > 6 {
msg = msg[7:]
} else {
msg = ""
}
}
event.Arguments[len(event.Arguments)-1] = msg
}
if callbacks, ok := irc.events[event.Code]; ok {
if irc.VerboseCallbackHandler {
irc.Log.Printf("%v (%v) >> %#v\n", event.Code, len(callbacks), event)
}
for _, callback := range callbacks {
callback(event)
}
} else if irc.VerboseCallbackHandler {
irc.Log.Printf("%v (0) >> %#v\n", event.Code, event)
}
if callbacks, ok := irc.events["*"]; ok {
if irc.VerboseCallbackHandler {
irc.Log.Printf("%v (0) >> %#v\n", event.Code, event)
}
for _, callback := range callbacks {
callback(event)
}
}
}
// Set up some initial callbacks to handle the IRC/CTCP protocol.
func (irc *Connection) setupCallbacks() {
irc.events = make(map[string]map[int]func(*Event))
//Handle error events.
irc.AddCallback("ERROR", func(e *Event) { irc.Disconnect() })
//Handle ping events
irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) })
//Version handler
irc.AddCallback("CTCP_VERSION", func(e *Event) {
irc.SendRawf("NOTICE %s :\x01VERSION %s\x01", e.Nick, irc.Version)
})
irc.AddCallback("CTCP_USERINFO", func(e *Event) {
irc.SendRawf("NOTICE %s :\x01USERINFO %s\x01", e.Nick, irc.user)
})
irc.AddCallback("CTCP_CLIENTINFO", func(e *Event) {
irc.SendRawf("NOTICE %s :\x01CLIENTINFO PING VERSION TIME USERINFO CLIENTINFO\x01", e.Nick)
})
irc.AddCallback("CTCP_TIME", func(e *Event) {
ltime := time.Now()
irc.SendRawf("NOTICE %s :\x01TIME %s\x01", e.Nick, ltime.String())
})
irc.AddCallback("CTCP_PING", func(e *Event) { irc.SendRawf("NOTICE %s :\x01%s\x01", e.Nick, e.Message()) })
// 437: ERR_UNAVAILRESOURCE "<nick/channel> :Nick/channel is temporarily unavailable"
// Add a _ to current nick. If irc.nickcurrent is empty this cannot
// work. It has to be set somewhere first in case the nick is already
// taken or unavailable from the beginning.
irc.AddCallback("437", func(e *Event) {
// If irc.nickcurrent hasn't been set yet, set to irc.nick
if irc.nickcurrent == "" {
irc.nickcurrent = irc.nick
}
if len(irc.nickcurrent) > 8 {
irc.nickcurrent = "_" + irc.nickcurrent
} else {
irc.nickcurrent = irc.nickcurrent + "_"
}
irc.SendRawf("NICK %s", irc.nickcurrent)
})
// 433: ERR_NICKNAMEINUSE "<nick> :Nickname is already in use"
// Add a _ to current nick.
irc.AddCallback("433", func(e *Event) {
// If irc.nickcurrent hasn't been set yet, set to irc.nick
if irc.nickcurrent == "" {
irc.nickcurrent = irc.nick
}
if len(irc.nickcurrent) > 8 {
irc.nickcurrent = "_" + irc.nickcurrent
} else {
irc.nickcurrent = irc.nickcurrent + "_"
}
irc.SendRawf("NICK %s", irc.nickcurrent)
})
irc.AddCallback("PONG", func(e *Event) {
ns, _ := strconv.ParseInt(e.Message(), 10, 64)
delta := time.Duration(time.Now().UnixNano() - ns)
if irc.Debug {
irc.Log.Printf("Lag: %.3f s\n", delta.Seconds())
}
})
// NICK Define a nickname.
// Set irc.nickcurrent to the new nick actually used in this connection.
irc.AddCallback("NICK", func(e *Event) {
if e.Nick == irc.nick {
irc.nickcurrent = e.Message()
}
})
// 1: RPL_WELCOME "Welcome to the Internet Relay Network <nick>!<user>@<host>"
// Set irc.nickcurrent to the actually used nick in this connection.
irc.AddCallback("001", func(e *Event) {
irc.Lock()
irc.nickcurrent = e.Arguments[0]
irc.Unlock()
})
}

53
vendor/github.com/42wim/go-ircevent/irc_sasl.go generated vendored Normal file
View File

@ -0,0 +1,53 @@
package irc
import (
"encoding/base64"
"errors"
"fmt"
"strings"
)
type SASLResult struct {
Failed bool
Err error
}
func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) {
irc.AddCallback("CAP", func(e *Event) {
if len(e.Arguments) == 3 {
if e.Arguments[1] == "LS" {
if !strings.Contains(e.Arguments[2], "sasl") {
result <- &SASLResult{true, errors.New("no SASL capability " + e.Arguments[2])}
}
}
if e.Arguments[1] == "ACK" {
if irc.SASLMech != "PLAIN" {
result <- &SASLResult{true, errors.New("only PLAIN is supported")}
}
irc.SendRaw("AUTHENTICATE " + irc.SASLMech)
}
}
})
irc.AddCallback("AUTHENTICATE", func(e *Event) {
str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword)))
irc.SendRaw("AUTHENTICATE " + str)
})
irc.AddCallback("901", func(e *Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
})
irc.AddCallback("902", func(e *Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
})
irc.AddCallback("903", func(e *Event) {
result <- &SASLResult{false, nil}
})
irc.AddCallback("904", func(e *Event) {
irc.SendRaw("CAP END")
irc.SendRaw("QUIT")
result <- &SASLResult{true, errors.New(e.Arguments[1])}
})
}

76
vendor/github.com/42wim/go-ircevent/irc_struct.go generated vendored Normal file
View File

@ -0,0 +1,76 @@
// Copyright 2009 Thomas Jager <mail@jager.no> All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package irc
import (
"crypto/tls"
"log"
"net"
"sync"
"time"
)
type Connection struct {
sync.Mutex
sync.WaitGroup
Debug bool
Error chan error
Password string
UseTLS bool
UseSASL bool
RequestCaps []string
AcknowledgedCaps []string
SASLLogin string
SASLPassword string
SASLMech string
TLSConfig *tls.Config
Version string
Timeout time.Duration
PingFreq time.Duration
KeepAlive time.Duration
Server string
socket net.Conn
pwrite chan string
end chan struct{}
nick string //The nickname we want.
nickcurrent string //The nickname we currently have.
user string
registered bool
events map[string]map[int]func(*Event)
QuitMessage string
lastMessage time.Time
VerboseCallbackHandler bool
Log *log.Logger
stopped bool
quit bool //User called Quit, do not reconnect.
}
// A struct to represent an event.
type Event struct {
Code string
Raw string
Nick string //<nick>
Host string //<nick>!<usr>@<host>
Source string //<host>
User string //<usr>
Arguments []string
Tags map[string]string
Connection *Connection
}
// Retrieve the last message from Event arguments.
// This function leaves the arguments untouched and
// returns an empty string if there are none.
func (e *Event) Message() string {
if len(e.Arguments) == 0 {
return ""
}
return e.Arguments[len(e.Arguments)-1]
}

14
vendor/github.com/42wim/go-ircevent/irc_test_fuzz.go generated vendored Normal file
View File

@ -0,0 +1,14 @@
// +build gofuzz
package irc
func Fuzz(data []byte) int {
b := bytes.NewBuffer(data)
event, err := parseToEvent(b.String())
if err == nil {
irc := IRC("go-eventirc", "go-eventirc")
irc.RunCallbacks(event)
return 1
}
return 0
}

View File

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

View File

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

View File

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

View File

@ -1,155 +0,0 @@
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()

14
vendor/github.com/BurntSushi/toml/COPYING generated vendored Normal file
View File

@ -0,0 +1,14 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

View File

@ -0,0 +1,90 @@
// Command toml-test-decoder satisfies the toml-test interface for testing
// TOML decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path"
"time"
"github.com/BurntSushi/toml"
)
func init() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() != 0 {
flag.Usage()
}
var tmp interface{}
if _, err := toml.DecodeReader(os.Stdin, &tmp); err != nil {
log.Fatalf("Error decoding TOML: %s", err)
}
typedTmp := translate(tmp)
if err := json.NewEncoder(os.Stdout).Encode(typedTmp); err != nil {
log.Fatalf("Error encoding JSON: %s", err)
}
}
func translate(tomlData interface{}) interface{} {
switch orig := tomlData.(type) {
case map[string]interface{}:
typed := make(map[string]interface{}, len(orig))
for k, v := range orig {
typed[k] = translate(v)
}
return typed
case []map[string]interface{}:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v).(map[string]interface{})
}
return typed
case []interface{}:
typed := make([]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v)
}
// We don't really need to tag arrays, but let's be future proof.
// (If TOML ever supports tuples, we'll need this.)
return tag("array", typed)
case time.Time:
return tag("datetime", orig.Format("2006-01-02T15:04:05Z"))
case bool:
return tag("bool", fmt.Sprintf("%v", orig))
case int64:
return tag("integer", fmt.Sprintf("%d", orig))
case float64:
return tag("float", fmt.Sprintf("%v", orig))
case string:
return tag("string", orig)
}
panic(fmt.Sprintf("Unknown type: %T", tomlData))
}
func tag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
}
}

View File

@ -0,0 +1,131 @@
// Command toml-test-encoder satisfies the toml-test interface for testing
// TOML encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
package main
import (
"encoding/json"
"flag"
"log"
"os"
"path"
"strconv"
"time"
"github.com/BurntSushi/toml"
)
func init() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() != 0 {
flag.Usage()
}
var tmp interface{}
if err := json.NewDecoder(os.Stdin).Decode(&tmp); err != nil {
log.Fatalf("Error decoding JSON: %s", err)
}
tomlData := translate(tmp)
if err := toml.NewEncoder(os.Stdout).Encode(tomlData); err != nil {
log.Fatalf("Error encoding TOML: %s", err)
}
}
func translate(typedJson interface{}) interface{} {
switch v := typedJson.(type) {
case map[string]interface{}:
if len(v) == 2 && in("type", v) && in("value", v) {
return untag(v)
}
m := make(map[string]interface{}, len(v))
for k, v2 := range v {
m[k] = translate(v2)
}
return m
case []interface{}:
tabArray := make([]map[string]interface{}, len(v))
for i := range v {
if m, ok := translate(v[i]).(map[string]interface{}); ok {
tabArray[i] = m
} else {
log.Fatalf("JSON arrays may only contain objects. This " +
"corresponds to only tables being allowed in " +
"TOML table arrays.")
}
}
return tabArray
}
log.Fatalf("Unrecognized JSON format '%T'.", typedJson)
panic("unreachable")
}
func untag(typed map[string]interface{}) interface{} {
t := typed["type"].(string)
v := typed["value"]
switch t {
case "string":
return v.(string)
case "integer":
v := v.(string)
n, err := strconv.Atoi(v)
if err != nil {
log.Fatalf("Could not parse '%s' as integer: %s", v, err)
}
return n
case "float":
v := v.(string)
f, err := strconv.ParseFloat(v, 64)
if err != nil {
log.Fatalf("Could not parse '%s' as float64: %s", v, err)
}
return f
case "datetime":
v := v.(string)
t, err := time.Parse("2006-01-02T15:04:05Z", v)
if err != nil {
log.Fatalf("Could not parse '%s' as a datetime: %s", v, err)
}
return t
case "bool":
v := v.(string)
switch v {
case "true":
return true
case "false":
return false
}
log.Fatalf("Could not parse '%s' as a boolean.", v)
case "array":
v := v.([]interface{})
array := make([]interface{}, len(v))
for i := range v {
if m, ok := v[i].(map[string]interface{}); ok {
array[i] = untag(m)
} else {
log.Fatalf("Arrays may only contain other arrays or "+
"primitive values, but found a '%T'.", m)
}
}
return array
}
log.Fatalf("Unrecognized tag type '%s'.", t)
panic("unreachable")
}
func in(key string, m map[string]interface{}) bool {
_, ok := m[key]
return ok
}

61
vendor/github.com/BurntSushi/toml/cmd/tomlv/main.go generated vendored Normal file
View File

@ -0,0 +1,61 @@
// Command tomlv validates TOML documents and prints each key's type.
package main
import (
"flag"
"fmt"
"log"
"os"
"path"
"strings"
"text/tabwriter"
"github.com/BurntSushi/toml"
)
var (
flagTypes = false
)
func init() {
log.SetFlags(0)
flag.BoolVar(&flagTypes, "types", flagTypes,
"When set, the types of every defined key will be shown.")
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s toml-file [ toml-file ... ]\n",
path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() < 1 {
flag.Usage()
}
for _, f := range flag.Args() {
var tmp interface{}
md, err := toml.DecodeFile(f, &tmp)
if err != nil {
log.Fatalf("Error in '%s': %s", f, err)
}
if flagTypes {
printTypes(md)
}
}
}
func printTypes(md toml.MetaData) {
tabw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
for _, key := range md.Keys() {
fmt.Fprintf(tabw, "%s%s\t%s\n",
strings.Repeat(" ", len(key)-1), key, md.Type(key...))
}
tabw.Flush()
}

509
vendor/github.com/BurntSushi/toml/decode.go generated vendored Normal file
View File

@ -0,0 +1,509 @@
package toml
import (
"fmt"
"io"
"io/ioutil"
"math"
"reflect"
"strings"
"time"
)
func e(format string, args ...interface{}) error {
return fmt.Errorf("toml: "+format, args...)
}
// Unmarshaler is the interface implemented by objects that can unmarshal a
// TOML description of themselves.
type Unmarshaler interface {
UnmarshalTOML(interface{}) error
}
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`.
func Unmarshal(p []byte, v interface{}) error {
_, err := Decode(string(p), v)
return err
}
// Primitive is a TOML value that hasn't been decoded into a Go value.
// When using the various `Decode*` functions, the type `Primitive` may
// be given to any value, and its decoding will be delayed.
//
// A `Primitive` value can be decoded using the `PrimitiveDecode` function.
//
// The underlying representation of a `Primitive` value is subject to change.
// Do not rely on it.
//
// N.B. Primitive values are still parsed, so using them will only avoid
// the overhead of reflection. They can be useful when you don't know the
// exact type of TOML data until run time.
type Primitive struct {
undecoded interface{}
context Key
}
// DEPRECATED!
//
// Use MetaData.PrimitiveDecode instead.
func PrimitiveDecode(primValue Primitive, v interface{}) error {
md := MetaData{decoded: make(map[string]bool)}
return md.unify(primValue.undecoded, rvalue(v))
}
// PrimitiveDecode is just like the other `Decode*` functions, except it
// decodes a TOML value that has already been parsed. Valid primitive values
// can *only* be obtained from values filled by the decoder functions,
// including this method. (i.e., `v` may contain more `Primitive`
// values.)
//
// Meta data for primitive values is included in the meta data returned by
// the `Decode*` functions with one exception: keys returned by the Undecoded
// method will only reflect keys that were decoded. Namely, any keys hidden
// behind a Primitive will be considered undecoded. Executing this method will
// update the undecoded keys in the meta data. (See the example.)
func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
md.context = primValue.context
defer func() { md.context = nil }()
return md.unify(primValue.undecoded, rvalue(v))
}
// Decode will decode the contents of `data` in TOML format into a pointer
// `v`.
//
// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be
// used interchangeably.)
//
// TOML arrays of tables correspond to either a slice of structs or a slice
// of maps.
//
// TOML datetimes correspond to Go `time.Time` values.
//
// All other TOML types (float, string, int, bool and array) correspond
// to the obvious Go types.
//
// An exception to the above rules is if a type implements the
// encoding.TextUnmarshaler interface. In this case, any primitive TOML value
// (floats, strings, integers, booleans and datetimes) will be converted to
// a byte string and given to the value's UnmarshalText method. See the
// Unmarshaler example for a demonstration with time duration strings.
//
// Key mapping
//
// TOML keys can map to either keys in a Go map or field names in a Go
// struct. The special `toml` struct tag may be used to map TOML keys to
// struct fields that don't match the key name exactly. (See the example.)
// A case insensitive match to struct names will be tried if an exact match
// can't be found.
//
// The mapping between TOML values and Go values is loose. That is, there
// may exist TOML values that cannot be placed into your representation, and
// there may be parts of your representation that do not correspond to
// TOML values. This loose mapping can be made stricter by using the IsDefined
// and/or Undecoded methods on the MetaData returned.
//
// This decoder will not handle cyclic types. If a cyclic type is passed,
// `Decode` will not terminate.
func Decode(data string, v interface{}) (MetaData, error) {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr {
return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v))
}
if rv.IsNil() {
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
}
p, err := parse(data)
if err != nil {
return MetaData{}, err
}
md := MetaData{
p.mapping, p.types, p.ordered,
make(map[string]bool, len(p.ordered)), nil,
}
return md, md.unify(p.mapping, indirect(rv))
}
// DecodeFile is just like Decode, except it will automatically read the
// contents of the file at `fpath` and decode it for you.
func DecodeFile(fpath string, v interface{}) (MetaData, error) {
bs, err := ioutil.ReadFile(fpath)
if err != nil {
return MetaData{}, err
}
return Decode(string(bs), v)
}
// DecodeReader is just like Decode, except it will consume all bytes
// from the reader and decode it for you.
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
bs, err := ioutil.ReadAll(r)
if err != nil {
return MetaData{}, err
}
return Decode(string(bs), v)
}
// unify performs a sort of type unification based on the structure of `rv`,
// which is the client representation.
//
// Any type mismatch produces an error. Finding a type that we don't know
// how to handle produces an unsupported type error.
func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
// Special case. Look for a `Primitive` value.
if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() {
// Save the undecoded data and the key context into the primitive
// value.
context := make(Key, len(md.context))
copy(context, md.context)
rv.Set(reflect.ValueOf(Primitive{
undecoded: data,
context: context,
}))
return nil
}
// Special case. Unmarshaler Interface support.
if rv.CanAddr() {
if v, ok := rv.Addr().Interface().(Unmarshaler); ok {
return v.UnmarshalTOML(data)
}
}
// Special case. Handle time.Time values specifically.
// TODO: Remove this code when we decide to drop support for Go 1.1.
// This isn't necessary in Go 1.2 because time.Time satisfies the encoding
// interfaces.
if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) {
return md.unifyDatetime(data, rv)
}
// Special case. Look for a value satisfying the TextUnmarshaler interface.
if v, ok := rv.Interface().(TextUnmarshaler); ok {
return md.unifyText(data, v)
}
// BUG(burntsushi)
// The behavior here is incorrect whenever a Go type satisfies the
// encoding.TextUnmarshaler interface but also corresponds to a TOML
// hash or array. In particular, the unmarshaler should only be applied
// to primitive TOML values. But at this point, it will be applied to
// all kinds of values and produce an incorrect error whenever those values
// are hashes or arrays (including arrays of tables).
k := rv.Kind()
// laziness
if k >= reflect.Int && k <= reflect.Uint64 {
return md.unifyInt(data, rv)
}
switch k {
case reflect.Ptr:
elem := reflect.New(rv.Type().Elem())
err := md.unify(data, reflect.Indirect(elem))
if err != nil {
return err
}
rv.Set(elem)
return nil
case reflect.Struct:
return md.unifyStruct(data, rv)
case reflect.Map:
return md.unifyMap(data, rv)
case reflect.Array:
return md.unifyArray(data, rv)
case reflect.Slice:
return md.unifySlice(data, rv)
case reflect.String:
return md.unifyString(data, rv)
case reflect.Bool:
return md.unifyBool(data, rv)
case reflect.Interface:
// we only support empty interfaces.
if rv.NumMethod() > 0 {
return e("unsupported type %s", rv.Type())
}
return md.unifyAnything(data, rv)
case reflect.Float32:
fallthrough
case reflect.Float64:
return md.unifyFloat64(data, rv)
}
return e("unsupported type %s", rv.Kind())
}
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{})
if !ok {
if mapping == nil {
return nil
}
return e("type mismatch for %s: expected table but found %T",
rv.Type().String(), mapping)
}
for key, datum := range tmap {
var f *field
fields := cachedTypeFields(rv.Type())
for i := range fields {
ff := &fields[i]
if ff.name == key {
f = ff
break
}
if f == nil && strings.EqualFold(ff.name, key) {
f = ff
}
}
if f != nil {
subv := rv
for _, i := range f.index {
subv = indirect(subv.Field(i))
}
if isUnifiable(subv) {
md.decoded[md.context.add(key).String()] = true
md.context = append(md.context, key)
if err := md.unify(datum, subv); err != nil {
return err
}
md.context = md.context[0 : len(md.context)-1]
} else if f.name != "" {
// Bad user! No soup for you!
return e("cannot write unexported field %s.%s",
rv.Type().String(), f.name)
}
}
}
return nil
}
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{})
if !ok {
if tmap == nil {
return nil
}
return badtype("map", mapping)
}
if rv.IsNil() {
rv.Set(reflect.MakeMap(rv.Type()))
}
for k, v := range tmap {
md.decoded[md.context.add(k).String()] = true
md.context = append(md.context, k)
rvkey := indirect(reflect.New(rv.Type().Key()))
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
if err := md.unify(v, rvval); err != nil {
return err
}
md.context = md.context[0 : len(md.context)-1]
rvkey.SetString(k)
rv.SetMapIndex(rvkey, rvval)
}
return nil
}
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice {
if !datav.IsValid() {
return nil
}
return badtype("slice", data)
}
sliceLen := datav.Len()
if sliceLen != rv.Len() {
return e("expected array length %d; got TOML array of length %d",
rv.Len(), sliceLen)
}
return md.unifySliceArray(datav, rv)
}
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice {
if !datav.IsValid() {
return nil
}
return badtype("slice", data)
}
n := datav.Len()
if rv.IsNil() || rv.Cap() < n {
rv.Set(reflect.MakeSlice(rv.Type(), n, n))
}
rv.SetLen(n)
return md.unifySliceArray(datav, rv)
}
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
sliceLen := data.Len()
for i := 0; i < sliceLen; i++ {
v := data.Index(i).Interface()
sliceval := indirect(rv.Index(i))
if err := md.unify(v, sliceval); err != nil {
return err
}
}
return nil
}
func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error {
if _, ok := data.(time.Time); ok {
rv.Set(reflect.ValueOf(data))
return nil
}
return badtype("time.Time", data)
}
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
if s, ok := data.(string); ok {
rv.SetString(s)
return nil
}
return badtype("string", data)
}
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
if num, ok := data.(float64); ok {
switch rv.Kind() {
case reflect.Float32:
fallthrough
case reflect.Float64:
rv.SetFloat(num)
default:
panic("bug")
}
return nil
}
return badtype("float", data)
}
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
if num, ok := data.(int64); ok {
if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 {
switch rv.Kind() {
case reflect.Int, reflect.Int64:
// No bounds checking necessary.
case reflect.Int8:
if num < math.MinInt8 || num > math.MaxInt8 {
return e("value %d is out of range for int8", num)
}
case reflect.Int16:
if num < math.MinInt16 || num > math.MaxInt16 {
return e("value %d is out of range for int16", num)
}
case reflect.Int32:
if num < math.MinInt32 || num > math.MaxInt32 {
return e("value %d is out of range for int32", num)
}
}
rv.SetInt(num)
} else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 {
unum := uint64(num)
switch rv.Kind() {
case reflect.Uint, reflect.Uint64:
// No bounds checking necessary.
case reflect.Uint8:
if num < 0 || unum > math.MaxUint8 {
return e("value %d is out of range for uint8", num)
}
case reflect.Uint16:
if num < 0 || unum > math.MaxUint16 {
return e("value %d is out of range for uint16", num)
}
case reflect.Uint32:
if num < 0 || unum > math.MaxUint32 {
return e("value %d is out of range for uint32", num)
}
}
rv.SetUint(unum)
} else {
panic("unreachable")
}
return nil
}
return badtype("integer", data)
}
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
if b, ok := data.(bool); ok {
rv.SetBool(b)
return nil
}
return badtype("boolean", data)
}
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
rv.Set(reflect.ValueOf(data))
return nil
}
func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
var s string
switch sdata := data.(type) {
case TextMarshaler:
text, err := sdata.MarshalText()
if err != nil {
return err
}
s = string(text)
case fmt.Stringer:
s = sdata.String()
case string:
s = sdata
case bool:
s = fmt.Sprintf("%v", sdata)
case int64:
s = fmt.Sprintf("%d", sdata)
case float64:
s = fmt.Sprintf("%f", sdata)
default:
return badtype("primitive (string-like)", data)
}
if err := v.UnmarshalText([]byte(s)); err != nil {
return err
}
return nil
}
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
func rvalue(v interface{}) reflect.Value {
return indirect(reflect.ValueOf(v))
}
// indirect returns the value pointed to by a pointer.
// Pointers are followed until the value is not a pointer.
// New values are allocated for each nil pointer.
//
// An exception to this rule is if the value satisfies an interface of
// interest to us (like encoding.TextUnmarshaler).
func indirect(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Ptr {
if v.CanSet() {
pv := v.Addr()
if _, ok := pv.Interface().(TextUnmarshaler); ok {
return pv
}
}
return v
}
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
return indirect(reflect.Indirect(v))
}
func isUnifiable(rv reflect.Value) bool {
if rv.CanSet() {
return true
}
if _, ok := rv.Interface().(TextUnmarshaler); ok {
return true
}
return false
}
func badtype(expected string, data interface{}) error {
return e("cannot load TOML value of type %T into a Go %s", data, expected)
}

121
vendor/github.com/BurntSushi/toml/decode_meta.go generated vendored Normal file
View File

@ -0,0 +1,121 @@
package toml
import "strings"
// MetaData allows access to meta information about TOML data that may not
// be inferrable via reflection. In particular, whether a key has been defined
// and the TOML type of a key.
type MetaData struct {
mapping map[string]interface{}
types map[string]tomlType
keys []Key
decoded map[string]bool
context Key // Used only during decoding.
}
// IsDefined returns true if the key given exists in the TOML data. The key
// should be specified hierarchially. e.g.,
//
// // access the TOML key 'a.b.c'
// IsDefined("a", "b", "c")
//
// IsDefined will return false if an empty key given. Keys are case sensitive.
func (md *MetaData) IsDefined(key ...string) bool {
if len(key) == 0 {
return false
}
var hash map[string]interface{}
var ok bool
var hashOrVal interface{} = md.mapping
for _, k := range key {
if hash, ok = hashOrVal.(map[string]interface{}); !ok {
return false
}
if hashOrVal, ok = hash[k]; !ok {
return false
}
}
return true
}
// Type returns a string representation of the type of the key specified.
//
// Type will return the empty string if given an empty key or a key that
// does not exist. Keys are case sensitive.
func (md *MetaData) Type(key ...string) string {
fullkey := strings.Join(key, ".")
if typ, ok := md.types[fullkey]; ok {
return typ.typeString()
}
return ""
}
// Key is the type of any TOML key, including key groups. Use (MetaData).Keys
// to get values of this type.
type Key []string
func (k Key) String() string {
return strings.Join(k, ".")
}
func (k Key) maybeQuotedAll() string {
var ss []string
for i := range k {
ss = append(ss, k.maybeQuoted(i))
}
return strings.Join(ss, ".")
}
func (k Key) maybeQuoted(i int) string {
quote := false
for _, c := range k[i] {
if !isBareKeyChar(c) {
quote = true
break
}
}
if quote {
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
}
return k[i]
}
func (k Key) add(piece string) Key {
newKey := make(Key, len(k)+1)
copy(newKey, k)
newKey[len(k)] = piece
return newKey
}
// Keys returns a slice of every key in the TOML data, including key groups.
// Each key is itself a slice, where the first element is the top of the
// hierarchy and the last is the most specific.
//
// The list will have the same order as the keys appeared in the TOML data.
//
// All keys returned are non-empty.
func (md *MetaData) Keys() []Key {
return md.keys
}
// Undecoded returns all keys that have not been decoded in the order in which
// they appear in the original TOML document.
//
// This includes keys that haven't been decoded because of a Primitive value.
// Once the Primitive value is decoded, the keys will be considered decoded.
//
// Also note that decoding into an empty interface will result in no decoding,
// and so no keys will be considered decoded.
//
// In this sense, the Undecoded keys correspond to keys in the TOML document
// that do not have a concrete type in your representation.
func (md *MetaData) Undecoded() []Key {
undecoded := make([]Key, 0, len(md.keys))
for _, key := range md.keys {
if !md.decoded[key.String()] {
undecoded = append(undecoded, key)
}
}
return undecoded
}

27
vendor/github.com/BurntSushi/toml/doc.go generated vendored Normal file
View File

@ -0,0 +1,27 @@
/*
Package toml provides facilities for decoding and encoding TOML configuration
files via reflection. There is also support for delaying decoding with
the Primitive type, and querying the set of keys in a TOML document with the
MetaData type.
The specification implemented: https://github.com/toml-lang/toml
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
whether a file is a valid TOML document. It can also be used to print the
type of each key in a TOML document.
Testing
There are two important types of tests used for this package. The first is
contained inside '*_test.go' files and uses the standard Go unit testing
framework. These tests are primarily devoted to holistically testing the
decoder and encoder.
The second type of testing is used to verify the implementation's adherence
to the TOML specification. These tests have been factored into their own
project: https://github.com/BurntSushi/toml-test
The reason the tests are in a separate project is so that they can be used by
any implementation of TOML. Namely, it is language agnostic.
*/
package toml

568
vendor/github.com/BurntSushi/toml/encode.go generated vendored Normal file
View File

@ -0,0 +1,568 @@
package toml
import (
"bufio"
"errors"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
type tomlEncodeError struct{ error }
var (
errArrayMixedElementTypes = errors.New(
"toml: cannot encode array with mixed element types")
errArrayNilElement = errors.New(
"toml: cannot encode array with nil element")
errNonString = errors.New(
"toml: cannot encode a map with non-string key type")
errAnonNonStruct = errors.New(
"toml: cannot encode an anonymous field that is not a struct")
errArrayNoTable = errors.New(
"toml: TOML array element cannot contain a table")
errNoKey = errors.New(
"toml: top-level values must be Go maps or structs")
errAnything = errors.New("") // used in testing
)
var quotedReplacer = strings.NewReplacer(
"\t", "\\t",
"\n", "\\n",
"\r", "\\r",
"\"", "\\\"",
"\\", "\\\\",
)
// Encoder controls the encoding of Go values to a TOML document to some
// io.Writer.
//
// The indentation level can be controlled with the Indent field.
type Encoder struct {
// A single indentation level. By default it is two spaces.
Indent string
// hasWritten is whether we have written any output to w yet.
hasWritten bool
w *bufio.Writer
}
// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer
// given. By default, a single indentation level is 2 spaces.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: bufio.NewWriter(w),
Indent: " ",
}
}
// Encode writes a TOML representation of the Go value to the underlying
// io.Writer. If the value given cannot be encoded to a valid TOML document,
// then an error is returned.
//
// The mapping between Go values and TOML values should be precisely the same
// as for the Decode* functions. Similarly, the TextMarshaler interface is
// supported by encoding the resulting bytes as strings. (If you want to write
// arbitrary binary data then you will need to use something like base64 since
// TOML does not have any binary types.)
//
// When encoding TOML hashes (i.e., Go maps or structs), keys without any
// sub-hashes are encoded first.
//
// If a Go map is encoded, then its keys are sorted alphabetically for
// deterministic output. More control over this behavior may be provided if
// there is demand for it.
//
// Encoding Go values without a corresponding TOML representation---like map
// types with non-string keys---will cause an error to be returned. Similarly
// for mixed arrays/slices, arrays/slices with nil elements, embedded
// non-struct types and nested slices containing maps or structs.
// (e.g., [][]map[string]string is not allowed but []map[string]string is OK
// and so is []map[string][]string.)
func (enc *Encoder) Encode(v interface{}) error {
rv := eindirect(reflect.ValueOf(v))
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
return err
}
return enc.w.Flush()
}
func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
defer func() {
if r := recover(); r != nil {
if terr, ok := r.(tomlEncodeError); ok {
err = terr.error
return
}
panic(r)
}
}()
enc.encode(key, rv)
return nil
}
func (enc *Encoder) encode(key Key, rv reflect.Value) {
// Special case. Time needs to be in ISO8601 format.
// Special case. If we can marshal the type to text, then we used that.
// Basically, this prevents the encoder for handling these types as
// generic structs (or whatever the underlying type of a TextMarshaler is).
switch rv.Interface().(type) {
case time.Time, TextMarshaler:
enc.keyEqElement(key, rv)
return
}
k := rv.Kind()
switch k {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64,
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
enc.keyEqElement(key, rv)
case reflect.Array, reflect.Slice:
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
enc.eArrayOfTables(key, rv)
} else {
enc.keyEqElement(key, rv)
}
case reflect.Interface:
if rv.IsNil() {
return
}
enc.encode(key, rv.Elem())
case reflect.Map:
if rv.IsNil() {
return
}
enc.eTable(key, rv)
case reflect.Ptr:
if rv.IsNil() {
return
}
enc.encode(key, rv.Elem())
case reflect.Struct:
enc.eTable(key, rv)
default:
panic(e("unsupported type for key '%s': %s", key, k))
}
}
// eElement encodes any value that can be an array element (primitives and
// arrays).
func (enc *Encoder) eElement(rv reflect.Value) {
switch v := rv.Interface().(type) {
case time.Time:
// Special case time.Time as a primitive. Has to come before
// TextMarshaler below because time.Time implements
// encoding.TextMarshaler, but we need to always use UTC.
enc.wf(v.UTC().Format("2006-01-02T15:04:05Z"))
return
case TextMarshaler:
// Special case. Use text marshaler if it's available for this value.
if s, err := v.MarshalText(); err != nil {
encPanic(err)
} else {
enc.writeQuoted(string(s))
}
return
}
switch rv.Kind() {
case reflect.Bool:
enc.wf(strconv.FormatBool(rv.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
enc.wf(strconv.FormatInt(rv.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
enc.wf(strconv.FormatUint(rv.Uint(), 10))
case reflect.Float32:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32)))
case reflect.Float64:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64)))
case reflect.Array, reflect.Slice:
enc.eArrayOrSliceElement(rv)
case reflect.Interface:
enc.eElement(rv.Elem())
case reflect.String:
enc.writeQuoted(rv.String())
default:
panic(e("unexpected primitive type: %s", rv.Kind()))
}
}
// By the TOML spec, all floats must have a decimal with at least one
// number on either side.
func floatAddDecimal(fstr string) string {
if !strings.Contains(fstr, ".") {
return fstr + ".0"
}
return fstr
}
func (enc *Encoder) writeQuoted(s string) {
enc.wf("\"%s\"", quotedReplacer.Replace(s))
}
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
length := rv.Len()
enc.wf("[")
for i := 0; i < length; i++ {
elem := rv.Index(i)
enc.eElement(elem)
if i != length-1 {
enc.wf(", ")
}
}
enc.wf("]")
}
func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
for i := 0; i < rv.Len(); i++ {
trv := rv.Index(i)
if isNil(trv) {
continue
}
panicIfInvalidKey(key)
enc.newline()
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
enc.newline()
enc.eMapOrStruct(key, trv)
}
}
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
panicIfInvalidKey(key)
if len(key) == 1 {
// Output an extra newline between top-level tables.
// (The newline isn't written if nothing else has been written though.)
enc.newline()
}
if len(key) > 0 {
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll())
enc.newline()
}
enc.eMapOrStruct(key, rv)
}
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) {
switch rv := eindirect(rv); rv.Kind() {
case reflect.Map:
enc.eMap(key, rv)
case reflect.Struct:
enc.eStruct(key, rv)
default:
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
}
}
func (enc *Encoder) eMap(key Key, rv reflect.Value) {
rt := rv.Type()
if rt.Key().Kind() != reflect.String {
encPanic(errNonString)
}
// Sort keys so that we have deterministic output. And write keys directly
// underneath this key first, before writing sub-structs or sub-maps.
var mapKeysDirect, mapKeysSub []string
for _, mapKey := range rv.MapKeys() {
k := mapKey.String()
if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) {
mapKeysSub = append(mapKeysSub, k)
} else {
mapKeysDirect = append(mapKeysDirect, k)
}
}
var writeMapKeys = func(mapKeys []string) {
sort.Strings(mapKeys)
for _, mapKey := range mapKeys {
mrv := rv.MapIndex(reflect.ValueOf(mapKey))
if isNil(mrv) {
// Don't write anything for nil fields.
continue
}
enc.encode(key.add(mapKey), mrv)
}
}
writeMapKeys(mapKeysDirect)
writeMapKeys(mapKeysSub)
}
func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
// Write keys for fields directly under this key first, because if we write
// a field that creates a new table, then all keys under it will be in that
// table (not the one we're writing here).
rt := rv.Type()
var fieldsDirect, fieldsSub [][]int
var addFields func(rt reflect.Type, rv reflect.Value, start []int)
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
// skip unexported fields
if f.PkgPath != "" && !f.Anonymous {
continue
}
frv := rv.Field(i)
if f.Anonymous {
t := f.Type
switch t.Kind() {
case reflect.Struct:
// Treat anonymous struct fields with
// tag names as though they are not
// anonymous, like encoding/json does.
if getOptions(f.Tag).name == "" {
addFields(t, frv, f.Index)
continue
}
case reflect.Ptr:
if t.Elem().Kind() == reflect.Struct &&
getOptions(f.Tag).name == "" {
if !frv.IsNil() {
addFields(t.Elem(), frv.Elem(), f.Index)
}
continue
}
// Fall through to the normal field encoding logic below
// for non-struct anonymous fields.
}
}
if typeIsHash(tomlTypeOfGo(frv)) {
fieldsSub = append(fieldsSub, append(start, f.Index...))
} else {
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
}
}
}
addFields(rt, rv, nil)
var writeFields = func(fields [][]int) {
for _, fieldIndex := range fields {
sft := rt.FieldByIndex(fieldIndex)
sf := rv.FieldByIndex(fieldIndex)
if isNil(sf) {
// Don't write anything for nil fields.
continue
}
opts := getOptions(sft.Tag)
if opts.skip {
continue
}
keyName := sft.Name
if opts.name != "" {
keyName = opts.name
}
if opts.omitempty && isEmpty(sf) {
continue
}
if opts.omitzero && isZero(sf) {
continue
}
enc.encode(key.add(keyName), sf)
}
}
writeFields(fieldsDirect)
writeFields(fieldsSub)
}
// tomlTypeName returns the TOML type name of the Go value's type. It is
// used to determine whether the types of array elements are mixed (which is
// forbidden). If the Go value is nil, then it is illegal for it to be an array
// element, and valueIsNil is returned as true.
// Returns the TOML type of a Go value. The type may be `nil`, which means
// no concrete TOML type could be found.
func tomlTypeOfGo(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() {
return nil
}
switch rv.Kind() {
case reflect.Bool:
return tomlBool
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64:
return tomlInteger
case reflect.Float32, reflect.Float64:
return tomlFloat
case reflect.Array, reflect.Slice:
if typeEqual(tomlHash, tomlArrayType(rv)) {
return tomlArrayHash
}
return tomlArray
case reflect.Ptr, reflect.Interface:
return tomlTypeOfGo(rv.Elem())
case reflect.String:
return tomlString
case reflect.Map:
return tomlHash
case reflect.Struct:
switch rv.Interface().(type) {
case time.Time:
return tomlDatetime
case TextMarshaler:
return tomlString
default:
return tomlHash
}
default:
panic("unexpected reflect.Kind: " + rv.Kind().String())
}
}
// tomlArrayType returns the element type of a TOML array. The type returned
// may be nil if it cannot be determined (e.g., a nil slice or a zero length
// slize). This function may also panic if it finds a type that cannot be
// expressed in TOML (such as nil elements, heterogeneous arrays or directly
// nested arrays of tables).
func tomlArrayType(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
return nil
}
firstType := tomlTypeOfGo(rv.Index(0))
if firstType == nil {
encPanic(errArrayNilElement)
}
rvlen := rv.Len()
for i := 1; i < rvlen; i++ {
elem := rv.Index(i)
switch elemType := tomlTypeOfGo(elem); {
case elemType == nil:
encPanic(errArrayNilElement)
case !typeEqual(firstType, elemType):
encPanic(errArrayMixedElementTypes)
}
}
// If we have a nested array, then we must make sure that the nested
// array contains ONLY primitives.
// This checks arbitrarily nested arrays.
if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) {
nest := tomlArrayType(eindirect(rv.Index(0)))
if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) {
encPanic(errArrayNoTable)
}
}
return firstType
}
type tagOptions struct {
skip bool // "-"
name string
omitempty bool
omitzero bool
}
func getOptions(tag reflect.StructTag) tagOptions {
t := tag.Get("toml")
if t == "-" {
return tagOptions{skip: true}
}
var opts tagOptions
parts := strings.Split(t, ",")
opts.name = parts[0]
for _, s := range parts[1:] {
switch s {
case "omitempty":
opts.omitempty = true
case "omitzero":
opts.omitzero = true
}
}
return opts
}
func isZero(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return rv.Uint() == 0
case reflect.Float32, reflect.Float64:
return rv.Float() == 0.0
}
return false
}
func isEmpty(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return rv.Len() == 0
case reflect.Bool:
return !rv.Bool()
}
return false
}
func (enc *Encoder) newline() {
if enc.hasWritten {
enc.wf("\n")
}
}
func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
panicIfInvalidKey(key)
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.eElement(val)
enc.newline()
}
func (enc *Encoder) wf(format string, v ...interface{}) {
if _, err := fmt.Fprintf(enc.w, format, v...); err != nil {
encPanic(err)
}
enc.hasWritten = true
}
func (enc *Encoder) indentStr(key Key) string {
return strings.Repeat(enc.Indent, len(key)-1)
}
func encPanic(err error) {
panic(tomlEncodeError{err})
}
func eindirect(v reflect.Value) reflect.Value {
switch v.Kind() {
case reflect.Ptr, reflect.Interface:
return eindirect(v.Elem())
default:
return v
}
}
func isNil(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return rv.IsNil()
default:
return false
}
}
func panicIfInvalidKey(key Key) {
for _, k := range key {
if len(k) == 0 {
encPanic(e("Key '%s' is not a valid table name. Key names "+
"cannot be empty.", key.maybeQuotedAll()))
}
}
}
func isValidKeyName(s string) bool {
return len(s) != 0
}

19
vendor/github.com/BurntSushi/toml/encoding_types.go generated vendored Normal file
View File

@ -0,0 +1,19 @@
// +build go1.2
package toml
// In order to support Go 1.1, we define our own TextMarshaler and
// TextUnmarshaler types. For Go 1.2+, we just alias them with the
// standard library interfaces.
import (
"encoding"
)
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextMarshaler encoding.TextMarshaler
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
// here so that Go 1.1 can be supported.
type TextUnmarshaler encoding.TextUnmarshaler

View File

@ -0,0 +1,18 @@
// +build !go1.2
package toml
// These interfaces were introduced in Go 1.2, so we add them manually when
// compiling for Go 1.1.
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextMarshaler interface {
MarshalText() (text []byte, err error)
}
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
// here so that Go 1.1 can be supported.
type TextUnmarshaler interface {
UnmarshalText(text []byte) error
}

953
vendor/github.com/BurntSushi/toml/lex.go generated vendored Normal file
View File

@ -0,0 +1,953 @@
package toml
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
type itemType int
const (
itemError itemType = iota
itemNIL // used in the parser to indicate no type
itemEOF
itemText
itemString
itemRawString
itemMultilineString
itemRawMultilineString
itemBool
itemInteger
itemFloat
itemDatetime
itemArray // the start of an array
itemArrayEnd
itemTableStart
itemTableEnd
itemArrayTableStart
itemArrayTableEnd
itemKeyStart
itemCommentStart
itemInlineTableStart
itemInlineTableEnd
)
const (
eof = 0
comma = ','
tableStart = '['
tableEnd = ']'
arrayTableStart = '['
arrayTableEnd = ']'
tableSep = '.'
keySep = '='
arrayStart = '['
arrayEnd = ']'
commentStart = '#'
stringStart = '"'
stringEnd = '"'
rawStringStart = '\''
rawStringEnd = '\''
inlineTableStart = '{'
inlineTableEnd = '}'
)
type stateFn func(lx *lexer) stateFn
type lexer struct {
input string
start int
pos int
line int
state stateFn
items chan item
// Allow for backing up up to three runes.
// This is necessary because TOML contains 3-rune tokens (""" and ''').
prevWidths [3]int
nprev int // how many of prevWidths are in use
// If we emit an eof, we can still back up, but it is not OK to call
// next again.
atEOF bool
// A stack of state functions used to maintain context.
// The idea is to reuse parts of the state machine in various places.
// For example, values can appear at the top level or within arbitrarily
// nested arrays. The last state on the stack is used after a value has
// been lexed. Similarly for comments.
stack []stateFn
}
type item struct {
typ itemType
val string
line int
}
func (lx *lexer) nextItem() item {
for {
select {
case item := <-lx.items:
return item
default:
lx.state = lx.state(lx)
}
}
}
func lex(input string) *lexer {
lx := &lexer{
input: input,
state: lexTop,
line: 1,
items: make(chan item, 10),
stack: make([]stateFn, 0, 10),
}
return lx
}
func (lx *lexer) push(state stateFn) {
lx.stack = append(lx.stack, state)
}
func (lx *lexer) pop() stateFn {
if len(lx.stack) == 0 {
return lx.errorf("BUG in lexer: no states to pop")
}
last := lx.stack[len(lx.stack)-1]
lx.stack = lx.stack[0 : len(lx.stack)-1]
return last
}
func (lx *lexer) current() string {
return lx.input[lx.start:lx.pos]
}
func (lx *lexer) emit(typ itemType) {
lx.items <- item{typ, lx.current(), lx.line}
lx.start = lx.pos
}
func (lx *lexer) emitTrim(typ itemType) {
lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line}
lx.start = lx.pos
}
func (lx *lexer) next() (r rune) {
if lx.atEOF {
panic("next called after EOF")
}
if lx.pos >= len(lx.input) {
lx.atEOF = true
return eof
}
if lx.input[lx.pos] == '\n' {
lx.line++
}
lx.prevWidths[2] = lx.prevWidths[1]
lx.prevWidths[1] = lx.prevWidths[0]
if lx.nprev < 3 {
lx.nprev++
}
r, w := utf8.DecodeRuneInString(lx.input[lx.pos:])
lx.prevWidths[0] = w
lx.pos += w
return r
}
// ignore skips over the pending input before this point.
func (lx *lexer) ignore() {
lx.start = lx.pos
}
// backup steps back one rune. Can be called only twice between calls to next.
func (lx *lexer) backup() {
if lx.atEOF {
lx.atEOF = false
return
}
if lx.nprev < 1 {
panic("backed up too far")
}
w := lx.prevWidths[0]
lx.prevWidths[0] = lx.prevWidths[1]
lx.prevWidths[1] = lx.prevWidths[2]
lx.nprev--
lx.pos -= w
if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
lx.line--
}
}
// accept consumes the next rune if it's equal to `valid`.
func (lx *lexer) accept(valid rune) bool {
if lx.next() == valid {
return true
}
lx.backup()
return false
}
// peek returns but does not consume the next rune in the input.
func (lx *lexer) peek() rune {
r := lx.next()
lx.backup()
return r
}
// skip ignores all input that matches the given predicate.
func (lx *lexer) skip(pred func(rune) bool) {
for {
r := lx.next()
if pred(r) {
continue
}
lx.backup()
lx.ignore()
return
}
}
// errorf stops all lexing by emitting an error and returning `nil`.
// Note that any value that is a character is escaped if it's a special
// character (newlines, tabs, etc.).
func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
lx.items <- item{
itemError,
fmt.Sprintf(format, values...),
lx.line,
}
return nil
}
// lexTop consumes elements at the top level of TOML data.
func lexTop(lx *lexer) stateFn {
r := lx.next()
if isWhitespace(r) || isNL(r) {
return lexSkip(lx, lexTop)
}
switch r {
case commentStart:
lx.push(lexTop)
return lexCommentStart
case tableStart:
return lexTableStart
case eof:
if lx.pos > lx.start {
return lx.errorf("unexpected EOF")
}
lx.emit(itemEOF)
return nil
}
// At this point, the only valid item can be a key, so we back up
// and let the key lexer do the rest.
lx.backup()
lx.push(lexTopEnd)
return lexKeyStart
}
// lexTopEnd is entered whenever a top-level item has been consumed. (A value
// or a table.) It must see only whitespace, and will turn back to lexTop
// upon a newline. If it sees EOF, it will quit the lexer successfully.
func lexTopEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case r == commentStart:
// a comment will read to a newline for us.
lx.push(lexTop)
return lexCommentStart
case isWhitespace(r):
return lexTopEnd
case isNL(r):
lx.ignore()
return lexTop
case r == eof:
lx.emit(itemEOF)
return nil
}
return lx.errorf("expected a top-level item to end with a newline, "+
"comment, or EOF, but got %q instead", r)
}
// lexTable lexes the beginning of a table. Namely, it makes sure that
// it starts with a character other than '.' and ']'.
// It assumes that '[' has already been consumed.
// It also handles the case that this is an item in an array of tables.
// e.g., '[[name]]'.
func lexTableStart(lx *lexer) stateFn {
if lx.peek() == arrayTableStart {
lx.next()
lx.emit(itemArrayTableStart)
lx.push(lexArrayTableEnd)
} else {
lx.emit(itemTableStart)
lx.push(lexTableEnd)
}
return lexTableNameStart
}
func lexTableEnd(lx *lexer) stateFn {
lx.emit(itemTableEnd)
return lexTopEnd
}
func lexArrayTableEnd(lx *lexer) stateFn {
if r := lx.next(); r != arrayTableEnd {
return lx.errorf("expected end of table array name delimiter %q, "+
"but got %q instead", arrayTableEnd, r)
}
lx.emit(itemArrayTableEnd)
return lexTopEnd
}
func lexTableNameStart(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.peek(); {
case r == tableEnd || r == eof:
return lx.errorf("unexpected end of table name " +
"(table names cannot be empty)")
case r == tableSep:
return lx.errorf("unexpected table separator " +
"(table names cannot be empty)")
case r == stringStart || r == rawStringStart:
lx.ignore()
lx.push(lexTableNameEnd)
return lexValue // reuse string lexing
default:
return lexBareTableName
}
}
// lexBareTableName lexes the name of a table. It assumes that at least one
// valid character for the table has already been read.
func lexBareTableName(lx *lexer) stateFn {
r := lx.next()
if isBareKeyChar(r) {
return lexBareTableName
}
lx.backup()
lx.emit(itemText)
return lexTableNameEnd
}
// lexTableNameEnd reads the end of a piece of a table name, optionally
// consuming whitespace.
func lexTableNameEnd(lx *lexer) stateFn {
lx.skip(isWhitespace)
switch r := lx.next(); {
case isWhitespace(r):
return lexTableNameEnd
case r == tableSep:
lx.ignore()
return lexTableNameStart
case r == tableEnd:
return lx.pop()
default:
return lx.errorf("expected '.' or ']' to end table name, "+
"but got %q instead", r)
}
}
// lexKeyStart consumes a key name up until the first non-whitespace character.
// lexKeyStart will ignore whitespace.
func lexKeyStart(lx *lexer) stateFn {
r := lx.peek()
switch {
case r == keySep:
return lx.errorf("unexpected key separator %q", keySep)
case isWhitespace(r) || isNL(r):
lx.next()
return lexSkip(lx, lexKeyStart)
case r == stringStart || r == rawStringStart:
lx.ignore()
lx.emit(itemKeyStart)
lx.push(lexKeyEnd)
return lexValue // reuse string lexing
default:
lx.ignore()
lx.emit(itemKeyStart)
return lexBareKey
}
}
// lexBareKey consumes the text of a bare key. Assumes that the first character
// (which is not whitespace) has not yet been consumed.
func lexBareKey(lx *lexer) stateFn {
switch r := lx.next(); {
case isBareKeyChar(r):
return lexBareKey
case isWhitespace(r):
lx.backup()
lx.emit(itemText)
return lexKeyEnd
case r == keySep:
lx.backup()
lx.emit(itemText)
return lexKeyEnd
default:
return lx.errorf("bare keys cannot contain %q", r)
}
}
// lexKeyEnd consumes the end of a key and trims whitespace (up to the key
// separator).
func lexKeyEnd(lx *lexer) stateFn {
switch r := lx.next(); {
case r == keySep:
return lexSkip(lx, lexValue)
case isWhitespace(r):
return lexSkip(lx, lexKeyEnd)
default:
return lx.errorf("expected key separator %q, but got %q instead",
keySep, r)
}
}
// lexValue starts the consumption of a value anywhere a value is expected.
// lexValue will ignore whitespace.
// After a value is lexed, the last state on the next is popped and returned.
func lexValue(lx *lexer) stateFn {
// We allow whitespace to precede a value, but NOT newlines.
// In array syntax, the array states are responsible for ignoring newlines.
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexValue)
case isDigit(r):
lx.backup() // avoid an extra state and use the same as above
return lexNumberOrDateStart
}
switch r {
case arrayStart:
lx.ignore()
lx.emit(itemArray)
return lexArrayValue
case inlineTableStart:
lx.ignore()
lx.emit(itemInlineTableStart)
return lexInlineTableValue
case stringStart:
if lx.accept(stringStart) {
if lx.accept(stringStart) {
lx.ignore() // Ignore """
return lexMultilineString
}
lx.backup()
}
lx.ignore() // ignore the '"'
return lexString
case rawStringStart:
if lx.accept(rawStringStart) {
if lx.accept(rawStringStart) {
lx.ignore() // Ignore """
return lexMultilineRawString
}
lx.backup()
}
lx.ignore() // ignore the "'"
return lexRawString
case '+', '-':
return lexNumberStart
case '.': // special error case, be kind to users
return lx.errorf("floats must start with a digit, not '.'")
}
if unicode.IsLetter(r) {
// Be permissive here; lexBool will give a nice error if the
// user wrote something like
// x = foo
// (i.e. not 'true' or 'false' but is something else word-like.)
lx.backup()
return lexBool
}
return lx.errorf("expected value but found %q instead", r)
}
// lexArrayValue consumes one value in an array. It assumes that '[' or ','
// have already been consumed. All whitespace and newlines are ignored.
func lexArrayValue(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r) || isNL(r):
return lexSkip(lx, lexArrayValue)
case r == commentStart:
lx.push(lexArrayValue)
return lexCommentStart
case r == comma:
return lx.errorf("unexpected comma")
case r == arrayEnd:
// NOTE(caleb): The spec isn't clear about whether you can have
// a trailing comma or not, so we'll allow it.
return lexArrayEnd
}
lx.backup()
lx.push(lexArrayValueEnd)
return lexValue
}
// lexArrayValueEnd consumes everything between the end of an array value and
// the next value (or the end of the array): it ignores whitespace and newlines
// and expects either a ',' or a ']'.
func lexArrayValueEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r) || isNL(r):
return lexSkip(lx, lexArrayValueEnd)
case r == commentStart:
lx.push(lexArrayValueEnd)
return lexCommentStart
case r == comma:
lx.ignore()
return lexArrayValue // move on to the next value
case r == arrayEnd:
return lexArrayEnd
}
return lx.errorf(
"expected a comma or array terminator %q, but got %q instead",
arrayEnd, r,
)
}
// lexArrayEnd finishes the lexing of an array.
// It assumes that a ']' has just been consumed.
func lexArrayEnd(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemArrayEnd)
return lx.pop()
}
// lexInlineTableValue consumes one key/value pair in an inline table.
// It assumes that '{' or ',' have already been consumed. Whitespace is ignored.
func lexInlineTableValue(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValue)
case isNL(r):
return lx.errorf("newlines not allowed within inline tables")
case r == commentStart:
lx.push(lexInlineTableValue)
return lexCommentStart
case r == comma:
return lx.errorf("unexpected comma")
case r == inlineTableEnd:
return lexInlineTableEnd
}
lx.backup()
lx.push(lexInlineTableValueEnd)
return lexKeyStart
}
// lexInlineTableValueEnd consumes everything between the end of an inline table
// key/value pair and the next pair (or the end of the table):
// it ignores whitespace and expects either a ',' or a '}'.
func lexInlineTableValueEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r):
return lexSkip(lx, lexInlineTableValueEnd)
case isNL(r):
return lx.errorf("newlines not allowed within inline tables")
case r == commentStart:
lx.push(lexInlineTableValueEnd)
return lexCommentStart
case r == comma:
lx.ignore()
return lexInlineTableValue
case r == inlineTableEnd:
return lexInlineTableEnd
}
return lx.errorf("expected a comma or an inline table terminator %q, "+
"but got %q instead", inlineTableEnd, r)
}
// lexInlineTableEnd finishes the lexing of an inline table.
// It assumes that a '}' has just been consumed.
func lexInlineTableEnd(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemInlineTableEnd)
return lx.pop()
}
// lexString consumes the inner contents of a string. It assumes that the
// beginning '"' has already been consumed and ignored.
func lexString(lx *lexer) stateFn {
r := lx.next()
switch {
case r == eof:
return lx.errorf("unexpected EOF")
case isNL(r):
return lx.errorf("strings cannot contain newlines")
case r == '\\':
lx.push(lexString)
return lexStringEscape
case r == stringEnd:
lx.backup()
lx.emit(itemString)
lx.next()
lx.ignore()
return lx.pop()
}
return lexString
}
// lexMultilineString consumes the inner contents of a string. It assumes that
// the beginning '"""' has already been consumed and ignored.
func lexMultilineString(lx *lexer) stateFn {
switch lx.next() {
case eof:
return lx.errorf("unexpected EOF")
case '\\':
return lexMultilineStringEscape
case stringEnd:
if lx.accept(stringEnd) {
if lx.accept(stringEnd) {
lx.backup()
lx.backup()
lx.backup()
lx.emit(itemMultilineString)
lx.next()
lx.next()
lx.next()
lx.ignore()
return lx.pop()
}
lx.backup()
}
}
return lexMultilineString
}
// lexRawString consumes a raw string. Nothing can be escaped in such a string.
// It assumes that the beginning "'" has already been consumed and ignored.
func lexRawString(lx *lexer) stateFn {
r := lx.next()
switch {
case r == eof:
return lx.errorf("unexpected EOF")
case isNL(r):
return lx.errorf("strings cannot contain newlines")
case r == rawStringEnd:
lx.backup()
lx.emit(itemRawString)
lx.next()
lx.ignore()
return lx.pop()
}
return lexRawString
}
// lexMultilineRawString consumes a raw string. Nothing can be escaped in such
// a string. It assumes that the beginning "'''" has already been consumed and
// ignored.
func lexMultilineRawString(lx *lexer) stateFn {
switch lx.next() {
case eof:
return lx.errorf("unexpected EOF")
case rawStringEnd:
if lx.accept(rawStringEnd) {
if lx.accept(rawStringEnd) {
lx.backup()
lx.backup()
lx.backup()
lx.emit(itemRawMultilineString)
lx.next()
lx.next()
lx.next()
lx.ignore()
return lx.pop()
}
lx.backup()
}
}
return lexMultilineRawString
}
// lexMultilineStringEscape consumes an escaped character. It assumes that the
// preceding '\\' has already been consumed.
func lexMultilineStringEscape(lx *lexer) stateFn {
// Handle the special case first:
if isNL(lx.next()) {
return lexMultilineString
}
lx.backup()
lx.push(lexMultilineString)
return lexStringEscape(lx)
}
func lexStringEscape(lx *lexer) stateFn {
r := lx.next()
switch r {
case 'b':
fallthrough
case 't':
fallthrough
case 'n':
fallthrough
case 'f':
fallthrough
case 'r':
fallthrough
case '"':
fallthrough
case '\\':
return lx.pop()
case 'u':
return lexShortUnicodeEscape
case 'U':
return lexLongUnicodeEscape
}
return lx.errorf("invalid escape character %q; only the following "+
"escape characters are allowed: "+
`\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX`, r)
}
func lexShortUnicodeEscape(lx *lexer) stateFn {
var r rune
for i := 0; i < 4; i++ {
r = lx.next()
if !isHexadecimal(r) {
return lx.errorf(`expected four hexadecimal digits after '\u', `+
"but got %q instead", lx.current())
}
}
return lx.pop()
}
func lexLongUnicodeEscape(lx *lexer) stateFn {
var r rune
for i := 0; i < 8; i++ {
r = lx.next()
if !isHexadecimal(r) {
return lx.errorf(`expected eight hexadecimal digits after '\U', `+
"but got %q instead", lx.current())
}
}
return lx.pop()
}
// lexNumberOrDateStart consumes either an integer, a float, or datetime.
func lexNumberOrDateStart(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexNumberOrDate
}
switch r {
case '_':
return lexNumber
case 'e', 'E':
return lexFloat
case '.':
return lx.errorf("floats must start with a digit, not '.'")
}
return lx.errorf("expected a digit but got %q", r)
}
// lexNumberOrDate consumes either an integer, float or datetime.
func lexNumberOrDate(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexNumberOrDate
}
switch r {
case '-':
return lexDatetime
case '_':
return lexNumber
case '.', 'e', 'E':
return lexFloat
}
lx.backup()
lx.emit(itemInteger)
return lx.pop()
}
// lexDatetime consumes a Datetime, to a first approximation.
// The parser validates that it matches one of the accepted formats.
func lexDatetime(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexDatetime
}
switch r {
case '-', 'T', ':', '.', 'Z':
return lexDatetime
}
lx.backup()
lx.emit(itemDatetime)
return lx.pop()
}
// lexNumberStart consumes either an integer or a float. It assumes that a sign
// has already been read, but that *no* digits have been consumed.
// lexNumberStart will move to the appropriate integer or float states.
func lexNumberStart(lx *lexer) stateFn {
// We MUST see a digit. Even floats have to start with a digit.
r := lx.next()
if !isDigit(r) {
if r == '.' {
return lx.errorf("floats must start with a digit, not '.'")
}
return lx.errorf("expected a digit but got %q", r)
}
return lexNumber
}
// lexNumber consumes an integer or a float after seeing the first digit.
func lexNumber(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexNumber
}
switch r {
case '_':
return lexNumber
case '.', 'e', 'E':
return lexFloat
}
lx.backup()
lx.emit(itemInteger)
return lx.pop()
}
// lexFloat consumes the elements of a float. It allows any sequence of
// float-like characters, so floats emitted by the lexer are only a first
// approximation and must be validated by the parser.
func lexFloat(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexFloat
}
switch r {
case '_', '.', '-', '+', 'e', 'E':
return lexFloat
}
lx.backup()
lx.emit(itemFloat)
return lx.pop()
}
// lexBool consumes a bool string: 'true' or 'false.
func lexBool(lx *lexer) stateFn {
var rs []rune
for {
r := lx.next()
if r == eof || isWhitespace(r) || isNL(r) {
lx.backup()
break
}
rs = append(rs, r)
}
s := string(rs)
switch s {
case "true", "false":
lx.emit(itemBool)
return lx.pop()
}
return lx.errorf("expected value but found %q instead", s)
}
// lexCommentStart begins the lexing of a comment. It will emit
// itemCommentStart and consume no characters, passing control to lexComment.
func lexCommentStart(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemCommentStart)
return lexComment
}
// lexComment lexes an entire comment. It assumes that '#' has been consumed.
// It will consume *up to* the first newline character, and pass control
// back to the last state on the stack.
func lexComment(lx *lexer) stateFn {
r := lx.peek()
if isNL(r) || r == eof {
lx.emit(itemText)
return lx.pop()
}
lx.next()
return lexComment
}
// lexSkip ignores all slurped input and moves on to the next state.
func lexSkip(lx *lexer, nextState stateFn) stateFn {
return func(lx *lexer) stateFn {
lx.ignore()
return nextState
}
}
// isWhitespace returns true if `r` is a whitespace character according
// to the spec.
func isWhitespace(r rune) bool {
return r == '\t' || r == ' '
}
func isNL(r rune) bool {
return r == '\n' || r == '\r'
}
func isDigit(r rune) bool {
return r >= '0' && r <= '9'
}
func isHexadecimal(r rune) bool {
return (r >= '0' && r <= '9') ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
func isBareKeyChar(r rune) bool {
return (r >= 'A' && r <= 'Z') ||
(r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') ||
r == '_' ||
r == '-'
}
func (itype itemType) String() string {
switch itype {
case itemError:
return "Error"
case itemNIL:
return "NIL"
case itemEOF:
return "EOF"
case itemText:
return "Text"
case itemString, itemRawString, itemMultilineString, itemRawMultilineString:
return "String"
case itemBool:
return "Bool"
case itemInteger:
return "Integer"
case itemFloat:
return "Float"
case itemDatetime:
return "DateTime"
case itemTableStart:
return "TableStart"
case itemTableEnd:
return "TableEnd"
case itemKeyStart:
return "KeyStart"
case itemArray:
return "Array"
case itemArrayEnd:
return "ArrayEnd"
case itemCommentStart:
return "CommentStart"
}
panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype)))
}
func (item item) String() string {
return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val)
}

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