Compare commits

..

2 Commits

Author SHA1 Message Date
MOZGIII
831b5b332f Switched to custom discordgo branch 2019-08-13 03:56:49 +03:00
MOZGIII
d93879bca5 Proper support for uploading files via webhooks for discord 2019-08-12 15:41:24 +03:00
3674 changed files with 247311 additions and 575885 deletions

View File

@@ -1,2 +0,0 @@
Dockerfile
tgs.Dockerfile

View File

@@ -1,3 +0,0 @@
go:
comments:
disabled: true

View File

@@ -1,7 +1,6 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve. (Check the FAQ on the wiki first) about: Create a report to help us improve. (Check the FAQ on the wiki first)
labels: bug
--- ---

View File

@@ -1,7 +1,6 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
labels: enhancement
--- ---

View File

@@ -1,71 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 16 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['go']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,57 +0,0 @@
name: Development
on: [push, pull_request]
jobs:
lint:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 20
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.29
args: "-v --new-from-rev HEAD~5"
test-build-upload:
strategy:
matrix:
go-version: [1.14.x, 1.15.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Test
run: go test ./... -mod=vendor
- name: Build
run: |
mkdir -p output/{win,lin,arm,mac}
VERSION=$(git describe --tags)
GOOS=linux GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64
GOOS=windows GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe
GOOS=darwin GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64
- name: Upload linux 64-bit
if: startsWith(matrix.go-version,'1.15')
uses: actions/upload-artifact@v2
with:
name: matterbridge-linux-64bit
path: output/lin
- name: Upload windows 64-bit
if: startsWith(matrix.go-version,'1.15')
uses: actions/upload-artifact@v2
with:
name: matterbridge-windows-64bit
path: output/win
- name: Upload darwin 64-bit
if: startsWith(matrix.go-version,'1.15')
uses: actions/upload-artifact@v2
with:
name: matterbridge-darwin-64bit
path: output/mac

3
.gitignore vendored
View File

@@ -1,6 +1,5 @@
# Exclude matterbridge binary # Exclude matterbridge binary
/matterbridge matterbridge
/matterbridge.exe
# Exclude configuration file # Exclude configuration file
matterbridge.toml matterbridge.toml

View File

@@ -23,7 +23,7 @@ run:
# default value is empty list, but next dirs are always skipped independently # default value is empty list, but next dirs are always skipped independently
# from this option's value: # from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs: gateway/bridgemap$ skip-dirs:
# which files to skip: they will be analyzed, but issues from them # which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is # won't be reported. Default value is empty list, but there is
@@ -174,15 +174,7 @@ linters:
- lll - lll
- maligned - maligned
- prealloc - prealloc
- wsl
- gomnd
- godox
- goerr113
- testpackage
- godot
- interfacer
- goheader
- noctx
# rules to deal with reported isues # rules to deal with reported isues
issues: issues:

View File

@@ -21,11 +21,7 @@ builds:
ldflags: ldflags:
- -s -w -X main.githash={{.ShortCommit}} - -s -w -X main.githash={{.ShortCommit}}
archives: archive:
-
id: matterbridge
builds:
- matterbridge
name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format: binary format: binary
files: files:

56
.travis.yml Normal file
View File

@@ -0,0 +1,56 @@
language: go
go_import_path: github.com/42wim/matterbridge
# We have everything vendored so this helps TravisCI not run `go get ...`.
install: true
git:
depth: 200
notifications:
email: false
branches:
only:
- master
- /.*/
jobs:
include:
- stage: lint
# Run linting in one Go environment only.
script: ./ci/lint.sh
go: 1.12.x
env:
- GO111MODULE=on
- GOLANGCI_VERSION="v1.17.1"
- stage: test
# Run tests in a combination of Go environments.
script: ./ci/test.sh
go: 1.11.x
env:
- GO111MODULE=off
- script: ./ci/test.sh
go: 1.11.x
env:
- GO111MODULE=on
- script: ./ci/test.sh
go: 1.12.x
env:
- GO111MODULE=on
- REPORT_COVERAGE=1
- BINDEPLOY=1
before_deploy: /bin/bash ci/bintray.sh
deploy:
on:
all_branches: true
condition: $BINDEPLOY = 1
provider: bintray
edge:
branch: v1.8.47
file: ci/deploy.json
user: 42wim
key:
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="

View File

@@ -1,16 +1,11 @@
FROM alpine AS builder FROM alpine:edge
ENTRYPOINT ["/bin/matterbridge"]
COPY . /go/src/github.com/42wim/matterbridge COPY . /go/src/github.com/42wim/matterbridge
RUN apk update && apk add go git gcc musl-dev \ RUN apk update && apk add go git gcc musl-dev ca-certificates \
&& cd /go/src/github.com/42wim/matterbridge \ && cd /go/src/github.com/42wim/matterbridge \
&& export GOPATH=/go \ && export GOPATH=/go \
&& go get \ && go get \
&& go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge && go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge \
&& rm -rf /go \
FROM alpine && apk del --purge git go gcc musl-dev
RUN apk --no-cache add ca-certificates mailcap
COPY --from=builder /bin/matterbridge /bin/matterbridge
RUN mkdir /etc/matterbridge \
&& touch /etc/matterbridge/matterbridge.toml \
&& ln -sf /matterbridge.toml /etc/matterbridge/matterbridge.toml
ENTRYPOINT ["/bin/matterbridge", "-conf", "/etc/matterbridge/matterbridge.toml"]

296
README.md
View File

@@ -9,29 +9,26 @@ Letting people be where they want to be.<br />
<sup> <sup>
[Discord][mb-discord] |
[Gitter][mb-gitter] | [Gitter][mb-gitter] |
[IRC][mb-irc] | [IRC][mb-irc] |
[Keybase][mb-keybase] | [Discord][mb-discord] |
[Matrix][mb-matrix] | [Matrix][mb-matrix] |
[Mattermost][mb-mattermost] |
[MSTeams][mb-msteams] |
[Rocket.Chat][mb-rocketchat] |
[Slack][mb-slack] | [Slack][mb-slack] |
[Telegram][mb-telegram] | [Mattermost][mb-mattermost] |
[Rocket.Chat][mb-rocketchat] |
[XMPP][mb-xmpp] |
[Twitch][mb-twitch] | [Twitch][mb-twitch] |
[WhatsApp][mb-whatsapp] | [WhatsApp][mb-whatsapp] |
[XMPP][mb-xmpp] |
[Zulip][mb-zulip] | [Zulip][mb-zulip] |
[Telegram][mb-telegram] |
And more... And more...
</sup> </sup>
--- ----
[![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest) [![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)
[![Maintainability](https://api.codeclimate.com/v1/badges/82dff70ef2ba85a6173a/maintainability)](https://codeclimate.com/github/42wim/matterbridge/maintainability) [![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 /> [![Test Coverage](https://api.codeclimate.com/v1/badges/82dff70ef2ba85a6173a/test_coverage)](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br />
<hr /> <hr />
</div> </div>
<div align="right"><sup> <div align="right"><sup>
@@ -44,161 +41,134 @@ And more...
</a> </a>
</p> </p>
# Table of Contents ### Table of Contents
* [Features](https://github.com/42wim/matterbridge/wiki/Features)
- [matterbridge](#matterbridge) * [Natively supported](#natively-supported)
- [Table of Contents](#table-of-contents) * [3rd party via matterbridge api](#3rd-party-via-matterbridge-api)
- [Features](#features) * [API](#API)
- [Natively supported](#natively-supported) * [Chat with us](#chat-with-us)
- [3rd party via matterbridge api](#3rd-party-via-matterbridge-api) * [Screenshots](https://github.com/42wim/matterbridge/wiki/)
- [API](#api) * [Installing/upgrading](#installing--upgrading)
- [Chat with us](#chat-with-us) * [Binaries](#binaries)
- [Screenshots](#screenshots) * [Building](#building)
- [Installing / upgrading](#installing--upgrading) * [Configuration](#configuration)
- [Binaries](#binaries) * [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config)
- [Packages](#packages) * [Settings](#settings)
- [Building](#building) * [Examples](#examples)
- [Configuration](#configuration) * [Running](#running)
- [Basic configuration](#basic-configuration) * [Docker](#docker)
- [Settings](#settings) * [Changelog](#changelog)
- [Advanced configuration](#advanced-configuration) * [FAQ](#faq)
- [Examples](#examples) * [Related projects](#related-projects)
- [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing) * [Articles](#articles)
- [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general) * [Thanks](#thanks)
- [Running](#running)
- [Docker](#docker)
- [Changelog](#changelog)
- [FAQ](#faq)
- [Related projects](#related-projects)
- [Articles](#articles)
- [Thanks](#thanks)
## Features ## Features
* [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols)
- [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols) * [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols)
- [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols) * [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes)
- [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes) * Preserves threading when possible
- Preserves threading when possible * [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling)
- [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling) * [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing)
- [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing) * [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)
- [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups) * [API](https://github.com/42wim/matterbridge/wiki/Features#api)
- [API](https://github.com/42wim/matterbridge/wiki/Features#api)
### Natively supported ### Natively supported
- [Discord](https://discordapp.com) * [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x
- [Gitter](https://gitter.im) * [IRC](http://www.mirc.com/servers.html)
- [IRC](http://www.mirc.com/servers.html) * [XMPP](https://xmpp.org)
- [Keybase](https://keybase.io) * [Gitter](https://gitter.im)
- [Matrix](https://matrix.org) * [Slack](https://slack.com)
- [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x * [Discord](https://discordapp.com)
- [Microsoft Teams](https://teams.microsoft.com) * [Telegram](https://telegram.org)
- [Mumble](https://www.mumble.info/) * [Rocket.chat](https://rocket.chat)
- [Nextcloud Talk](https://nextcloud.com/talk/) * [Matrix](https://matrix.org)
- [Rocket.chat](https://rocket.chat) * [Steam](https://store.steampowered.com/)
- [Slack](https://slack.com) * [Twitch](https://twitch.tv)
- [Ssh-chat](https://github.com/shazow/ssh-chat) * [Ssh-chat](https://github.com/shazow/ssh-chat)
- [Steam](https://store.steampowered.com/) * [WhatsApp](https://www.whatsapp.com/)
- [Telegram](https://telegram.org) * [Zulip](https://zulipchat.com)
- [Twitch](https://twitch.tv)
- [WhatsApp](https://www.whatsapp.com/)
- [XMPP](https://xmpp.org)
- [Zulip](https://zulipchat.com)
### 3rd party via matterbridge api ### 3rd party via matterbridge api
* [Minecraft](https://github.com/elytra/MatterLink)
- [Discourse](https://github.com/DeclanHoare/matterbabble) * [Reddit](https://github.com/bonehurtingjuice/mattereddit)
- [Facebook messenger](https://github.com/VictorNine/fbridge) * [Facebook messenger](https://github.com/VictorNine/fbridge)
- [Minecraft](https://github.com/elytra/MatterLink) * [Discourse](https://github.com/DeclanHoare/matterbabble)
- [Reddit](https://github.com/bonehurtingjuice/mattereddit)
- [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
- [MatterAMXX](https://github.com/GabeIggy/MatterAMXX)
### API ### API
The API is basic at the moment. The API is basic at the moment.
More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api). More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api).
Used by the projects below. Feel free to make a PR to add your project to this list. Used by the projects below. Feel free to make a PR to add your project to this list.
- [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat) * [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot) * [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
- [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support) * [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) * [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support) * [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
- [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod)
## Chat with us ## Chat with us
Questions or want to test on your favorite platform? Join below: Questions or want to test on your favorite platform? Join below:
- [Discord][mb-discord] * [Gitter][mb-gitter]
- [Gitter][mb-gitter] * [IRC][mb-irc]
- [IRC][mb-irc] * [Discord][mb-discord]
- [Keybase][mb-keybase] * [Matrix][mb-matrix]
- [Matrix][mb-matrix] * [Slack][mb-slack]
- [Mattermost][mb-mattermost] * [Mattermost][mb-mattermost]
- [Rocket.Chat][mb-rocketchat] * [Rocket.Chat][mb-rocketchat]
- [Slack][mb-slack] * [XMPP][mb-xmpp] (matterbridge@conference.jabber.de)
- [Telegram][mb-telegram] * [Twitch][mb-twitch]
- [Twitch][mb-twitch] * [Zulip][mb-zulip]
- [XMPP][mb-xmpp] (matterbridge@conference.jabber.de) * [Telegram][mb-telegram]
- [Zulip][mb-zulip]
## Screenshots ## Screenshots
See https://github.com/42wim/matterbridge/wiki
See <https://github.com/42wim/matterbridge/wiki>
## Installing / upgrading ## Installing / upgrading
### Binaries ### Binaries
* Latest stable release [v1.15.1](https://github.com/42wim/matterbridge/releases/latest)
- Latest stable release [v1.20.0](https://github.com/42wim/matterbridge/releases/latest) * Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
- Development releases (follows master) can be downloaded [here](https://github.com/42wim/matterbridge/actions) selecting the latest green build and then artifacts.
To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest) and follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest) and follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
### Packages ### Packages
* [Overview](https://repology.org/metapackage/matterbridge/versions)
- [Overview](https://repology.org/metapackage/matterbridge/versions)
- [snap](https://snapcraft.io/matterbridge)
## Building ## Building
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest) Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest)
If you really want to build from source, follow these instructions: If you really want to build from source, follow these instructions:
Go 1.12+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed. 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).
```bash After Go is setup, download matterbridge to your $GOPATH directory.
```
cd $GOPATH
go get github.com/42wim/matterbridge go get github.com/42wim/matterbridge
``` ```
You should now have matterbridge binary in the ~/go/bin directory: You should now have matterbridge binary in the bin directory:
```bash ```
$ ls ~/go/bin/ $ 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.
### Settings ### Settings
All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for each bridge. All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for each bridge.
### Advanced configuration ### Advanced configuration
* [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
- [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
### Examples ### Examples
#### Bridge mattermost (off-topic) - irc (#testing) #### Bridge mattermost (off-topic) - irc (#testing)
```toml ```toml
[irc] [irc]
[irc.freenode] [irc.freenode]
@@ -227,7 +197,6 @@ enable=true
``` ```
#### Bridge slack (#general) - discord (general) #### Bridge slack (#general) - discord (general)
```toml ```toml
[slack] [slack]
[slack.test] [slack.test]
@@ -259,7 +228,7 @@ RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
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.
```bash ```
Usage of ./matterbridge: Usage of ./matterbridge:
-conf string -conf string
config file (default "matterbridge.toml") config file (default "matterbridge.toml")
@@ -272,11 +241,12 @@ Usage of ./matterbridge:
``` ```
### Docker ### Docker
Create your matterbridge.toml file locally eg in `/tmp/matterbridge.toml`
Please take a look at the [Docker Wiki page](https://github.com/42wim/matterbridge/wiki/Deploy:-Docker) for more information. ```
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
@@ -284,30 +254,28 @@ See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.m
See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
## Related projects ## Related projects
* [FOSSRIT/infrastructure - roles/matterbridge](https://github.com/FOSSRIT/infrastructure/tree/master/roles/matterbridge) (Ansible role used to automate deployments of Matterbridge)
- [jwflory/ansible-role-matterbridge](https://galaxy.ansible.com/jwflory/matterbridge) (Ansible role to simplify deploying Matterbridge) * [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig)
- [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig) * [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
- [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer) * [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
- [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku) * [mattereddit](https://github.com/bonehurtingjuice/mattereddit)
- [mattereddit](https://github.com/bonehurtingjuice/mattereddit) * [matterlink](https://github.com/elytra/MatterLink)
- [matterlink](https://github.com/elytra/MatterLink) * [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost
- [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost * [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot) * [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) * [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
- [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge) * [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
## Articles ## Articles
* [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
- [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3) * https://mattermost.com/blog/connect-irc-to-mattermost/
- <https://mattermost.com/blog/connect-irc-to-mattermost/> * https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/
- <https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/> * https://blog.brightscout.com/top-10-mattermost-integrations/
- <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://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://kopano.com/blog/matterbridge-bridging-mattermost-chat/
- <https://www.stitcher.com/s/?eid=52382713> * https://www.stitcher.com/s/?eid=52382713
- <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/> * https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/
- <https://userlinux.net/mattermost-and-matterbridge.html>
## Thanks ## Thanks
@@ -319,42 +287,34 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
</p> </p>
Matterbridge wouldn't exist without these libraries: Matterbridge wouldn't exist without these libraries:
* discord - https://github.com/bwmarrin/discordgo
- discord - <https://github.com/bwmarrin/discordgo> * 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
- gozulipbot - <https://github.com/ifo/gozulipbot> * irc - https://github.com/lrstanley/girc
- gumble - <https://github.com/layeh/gumble> * mattermost - https://github.com/mattermost/mattermost-server
- irc - <https://github.com/lrstanley/girc> * matrix - https://github.com/matrix-org/gomatrix
- keybase - <https://github.com/keybase/go-keybase-chat-bot> * sshchat - https://github.com/shazow/ssh-chat
- matrix - <https://github.com/matrix-org/gomatrix> * slack - https://github.com/nlopes/slack
- mattermost - <https://github.com/mattermost/mattermost-server> * steam - https://github.com/Philipp15b/go-steam
- msgraph.go - <https://github.com/yaegashi/msgraph.go> * telegram - https://github.com/go-telegram-bot-api/telegram-bot-api
- mumble - <https://github.com/layeh/gumble> * xmpp - https://github.com/mattn/go-xmpp
- nctalk - <https://github.com/gary-kim/go-nc-talk> * whatsapp - https://github.com/Rhymen/go-whatsapp/
- slack - <https://github.com/nlopes/slack> * zulip - https://github.com/ifo/gozulipbot
- sshchat - <https://github.com/shazow/ssh-chat> * tengo - https://github.com/d5/tengo
- steam - <https://github.com/Philipp15b/go-steam>
- telegram - <https://github.com/go-telegram-bot-api/telegram-bot-api>
- tengo - <https://github.com/d5/tengo>
- whatsapp - <https://github.com/Rhymen/go-whatsapp>
- xmpp - <https://github.com/mattn/go-xmpp>
- zulip - <https://github.com/ifo/gozulipbot>
<!-- Links --> <!-- Links -->
[mb-discord]: https://discord.gg/AkKPtrQ
[mb-gitter]: https://gitter.im/42wim/matterbridge [mb-gitter]: https://gitter.im/42wim/matterbridge
[mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat [mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat
[mb-keybase]: https://keybase.io/team/matterbridge [mb-discord]: https://discord.gg/AkKPtrQ
[mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org [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-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e
[mb-msteams]: https://teams.microsoft.com/join/hj92x75gd3y7
[mb-rocketchat]: https://open.rocket.chat/channel/matterbridge [mb-rocketchat]: https://open.rocket.chat/channel/matterbridge
[mb-slack]: https://join.slack.com/t/matterbridgechat/shared_invite/zt-2ourq2h2-7YvyYBq2WFGC~~zEzA68_Q [mb-xmpp]: https://inverse.chat/
[mb-telegram]: https://t.me/Matterbridge
[mb-twitch]: https://www.twitch.tv/matterbridge [mb-twitch]: https://www.twitch.tv/matterbridge
[mb-whatsapp]: https://www.whatsapp.com/ [mb-whatsapp]: https://www.whatsapp.com/
[mb-xmpp]: https://inverse.chat/
[mb-zulip]: https://matterbridge.zulipchat.com/register/ [mb-zulip]: https://matterbridge.zulipchat.com/register/
[mb-telegram]: https://t.me/Matterbridge

View File

@@ -6,20 +6,17 @@ import (
"sync" "sync"
"time" "time"
"gopkg.in/olahol/melody.v1"
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
ring "github.com/zfjagann/golang-ring" "github.com/zfjagann/golang-ring"
) )
type API struct { type API struct {
Messages ring.Ring Messages ring.Ring
sync.RWMutex sync.RWMutex
*bridge.Config *bridge.Config
mrouter *melody.Melody
} }
type Message struct { type Message struct {
@@ -35,32 +32,6 @@ func New(cfg *bridge.Config) bridge.Bridger {
e := echo.New() e := echo.New()
e.HideBanner = true e.HideBanner = true
e.HidePort = true e.HidePort = true
b.mrouter = melody.New()
b.mrouter.HandleMessage(func(s *melody.Session, msg []byte) {
message := config.Message{}
err := json.Unmarshal(msg, &message)
if err != nil {
b.Log.Errorf("failed to decode message from byte[] '%s'", string(msg))
return
}
b.handleWebsocketMessage(message, s)
})
b.mrouter.HandleConnect(func(session *melody.Session) {
greet := b.getGreeting()
data, err := json.Marshal(greet)
if err != nil {
b.Log.Errorf("failed to encode message '%v'", greet)
return
}
err = session.Write(data)
if err != nil {
b.Log.Errorf("failed to write message '%s'", string(data))
return
}
// TODO: send message history buffer from `b.Messages` here
})
b.Messages = ring.Ring{} b.Messages = ring.Ring{}
if b.GetInt("Buffer") != 0 { if b.GetInt("Buffer") != 0 {
b.Messages.SetCapacity(b.GetInt("Buffer")) b.Messages.SetCapacity(b.GetInt("Buffer"))
@@ -70,17 +41,9 @@ func New(cfg *bridge.Config) bridge.Bridger {
return key == b.GetString("Token"), nil return key == b.GetString("Token"), nil
})) }))
} }
// Set RemoteNickFormat to a sane default
if !b.IsKeySet("RemoteNickFormat") {
b.Log.Debugln("RemoteNickFormat is unset, defaulting to \"{NICK}\"")
b.Config.Config.Viper().Set(b.GetConfigKey("RemoteNickFormat"), "{NICK}")
}
e.GET("/api/health", b.handleHealthcheck) 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.GET("/api/websocket", b.handleWebsocket)
e.POST("/api/message", b.handlePostMessage) e.POST("/api/message", b.handlePostMessage)
go func() { go func() {
if b.GetString("BindAddress") == "" { if b.GetString("BindAddress") == "" {
@@ -95,13 +58,13 @@ func New(cfg *bridge.Config) bridge.Bridger {
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) {
@@ -111,14 +74,7 @@ func (b *API) Send(msg config.Message) (string, error) {
if msg.Event == config.EventMsgDelete { if msg.Event == config.EventMsgDelete {
return "", nil return "", nil
} }
b.Log.Debugf("enqueueing message from %s on ring buffer", msg.Username) b.Messages.Enqueue(&msg)
b.Messages.Enqueue(msg)
data, err := json.Marshal(msg)
if err != nil {
b.Log.Errorf("failed to encode message '%s'", msg)
}
_ = b.mrouter.Broadcast(data)
return "", nil return "", nil
} }
@@ -150,23 +106,18 @@ func (b *API) handleMessages(c echo.Context) error {
return nil return nil
} }
func (b *API) getGreeting() config.Message {
return config.Message{
Event: config.EventAPIConnected,
Timestamp: time.Now(),
}
}
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 := b.getGreeting() greet := config.Message{
Event: config.EventAPIConnected,
Timestamp: time.Now(),
}
if err := json.NewEncoder(c.Response()).Encode(greet); err != nil { if err := json.NewEncoder(c.Response()).Encode(greet); err != nil {
return err return err
} }
c.Response().Flush() c.Response().Flush()
for { for {
// TODO: this causes issues, messages should be broadcasted to all connected clients
msg := b.Messages.Dequeue() msg := b.Messages.Dequeue()
if msg != nil { if msg != nil {
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil { if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
@@ -177,31 +128,3 @@ func (b *API) handleStream(c echo.Context) error {
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
} }
} }
func (b *API) handleWebsocketMessage(message config.Message, s *melody.Session) {
message.Channel = "api"
message.Protocol = "api"
message.Account = b.Account
message.ID = ""
message.Timestamp = time.Now()
data, err := json.Marshal(message)
if err != nil {
b.Log.Errorf("failed to encode message for loopback '%v'", message)
return
}
_ = b.mrouter.BroadcastOthers(data, s)
b.Log.Debugf("Sending websocket message from %s on %s to gateway", message.Username, "api")
b.Remote <- message
}
func (b *API) handleWebsocket(c echo.Context) error {
err := b.mrouter.HandleRequest(c.Response(), c.Request())
if err != nil {
b.Log.Errorf("error in websocket handling '%v'", err)
return err
}
return nil
}

View File

@@ -1,10 +1,8 @@
package bridge package bridge
import ( import (
"log"
"strings" "strings"
"sync" "sync"
"time"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@@ -43,10 +41,6 @@ type Factory func(*Config) Bridger
func New(bridge *config.Bridge) *Bridge { func New(bridge *config.Bridge) *Bridge {
accInfo := strings.Split(bridge.Account, ".") accInfo := strings.Split(bridge.Account, ".")
if len(accInfo) != 2 {
log.Fatalf("config failure, account incorrect: %s", bridge.Account)
}
protocol := accInfo[0] protocol := accInfo[0]
name := accInfo[1] name := accInfo[1]
@@ -75,7 +69,6 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map
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) b.Log.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID)
time.Sleep(time.Duration(b.GetInt("JoinDelay")) * time.Millisecond)
err := b.JoinChannel(channel) err := b.JoinChannel(channel)
if err != nil { if err != nil {
return err return err
@@ -86,16 +79,8 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map
return nil return nil
} }
func (b *Bridge) GetConfigKey(key string) string {
return b.Account + "." + key
}
func (b *Bridge) IsKeySet(key string) bool {
return b.Config.IsKeySet(b.GetConfigKey(key)) || b.Config.IsKeySet("general."+key)
}
func (b *Bridge) GetBool(key string) bool { func (b *Bridge) GetBool(key string) bool {
val, ok := b.Config.GetBool(b.GetConfigKey(key)) val, ok := b.Config.GetBool(b.Account + "." + key)
if !ok { if !ok {
val, _ = b.Config.GetBool("general." + key) val, _ = b.Config.GetBool("general." + key)
} }
@@ -103,7 +88,7 @@ func (b *Bridge) GetBool(key string) bool {
} }
func (b *Bridge) GetInt(key string) int { func (b *Bridge) GetInt(key string) int {
val, ok := b.Config.GetInt(b.GetConfigKey(key)) val, ok := b.Config.GetInt(b.Account + "." + key)
if !ok { if !ok {
val, _ = b.Config.GetInt("general." + key) val, _ = b.Config.GetInt("general." + key)
} }
@@ -111,7 +96,7 @@ func (b *Bridge) GetInt(key string) int {
} }
func (b *Bridge) GetString(key string) string { func (b *Bridge) GetString(key string) string {
val, ok := b.Config.GetString(b.GetConfigKey(key)) val, ok := b.Config.GetString(b.Account + "." + key)
if !ok { if !ok {
val, _ = b.Config.GetString("general." + key) val, _ = b.Config.GetString("general." + key)
} }
@@ -119,7 +104,7 @@ func (b *Bridge) GetString(key string) string {
} }
func (b *Bridge) GetStringSlice(key string) []string { func (b *Bridge) GetStringSlice(key string) []string {
val, ok := b.Config.GetStringSlice(b.GetConfigKey(key)) val, ok := b.Config.GetStringSlice(b.Account + "." + key)
if !ok { if !ok {
val, _ = b.Config.GetStringSlice("general." + key) val, _ = b.Config.GetStringSlice("general." + key)
} }
@@ -127,7 +112,7 @@ func (b *Bridge) GetStringSlice(key string) []string {
} }
func (b *Bridge) GetStringSlice2D(key string) [][]string { func (b *Bridge) GetStringSlice2D(key string) [][]string {
val, ok := b.Config.GetStringSlice2D(b.GetConfigKey(key)) val, ok := b.Config.GetStringSlice2D(b.Account + "." + key)
if !ok { if !ok {
val, _ = b.Config.GetStringSlice2D("general." + key) val, _ = b.Config.GetStringSlice2D("general." + key)
} }

View File

@@ -3,8 +3,6 @@ package config
import ( import (
"bytes" "bytes"
"io/ioutil" "io/ioutil"
"os"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -78,29 +76,23 @@ type Protocol struct {
BindAddress string // mattermost, slack // DEPRECATED BindAddress string // mattermost, slack // DEPRECATED
Buffer int // api Buffer int // api
Charset string // irc Charset string // irc
ClientID string // msteams
ColorNicks bool // only irc for now ColorNicks bool // only irc for now
Debug bool // general Debug bool // general
DebugLevel int // only for irc now DebugLevel int // only for irc now
DisableWebPagePreview bool // telegram
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
HTMLDisable bool // matrix
IconURL string // mattermost, slack IconURL string // mattermost, slack
IgnoreFailureOnStart bool // general 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
JoinDelay string // all protocols
Label string // all protocols Label string // all protocols
Login string // mattermost, matrix Login string // mattermost, matrix
LogFile string // general
MediaDownloadBlackList []string MediaDownloadBlackList []string
MediaDownloadPath string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server. 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
MediaConvertTgs string // telegram
MediaConvertWebPToPNG bool // telegram 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
@@ -117,21 +109,19 @@ type Protocol struct {
NicksPerRow int // mattermost, slack NicksPerRow int // mattermost, slack
NoHomeServerSuffix bool // matrix NoHomeServerSuffix bool // matrix
NoSendJoinPart bool // all protocols NoSendJoinPart bool // all protocols
NoTLS bool // mattermost, xmpp 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 PreserveThreading bool // slack
Protocol string // all protocols Protocol string // all protocols
QuoteDisable bool // telegram QuoteDisable bool // telegram
QuoteFormat string // telegram QuoteFormat string // telegram
QuoteLengthLimit int // 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 RunCommands []string // IRC
Server string // IRC,mattermost,XMPP,discord Server string // IRC,mattermost,XMPP,discord
SessionFile string // msteams,whatsapp
ShowJoinPart bool // all protocols ShowJoinPart bool // all protocols
ShowTopicChange bool // slack ShowTopicChange bool // slack
ShowUserTyping bool // slack ShowUserTyping bool // slack
@@ -139,22 +129,18 @@ type Protocol struct {
SkipTLSVerify bool // IRC, mattermost SkipTLSVerify bool // IRC, mattermost
SkipVersionCheck bool // mattermost SkipVersionCheck bool // mattermost
StripNick bool // all protocols StripNick bool // all protocols
StripMarkdown bool // irc
SyncTopic bool // slack SyncTopic bool // slack
TengoModifyMessage string // general TengoModifyMessage string // general
Team string // mattermost, keybase Team string // mattermost
TeamID string // msteams
TenantID string // msteams
Token string // gitter, slack, discord, api Token string // gitter, slack, discord, api
Topic string // zulip Topic string // zulip
URL string // mattermost, slack // DEPRECATED URL string // mattermost, slack // DEPRECATED
UseAPI bool // mattermost, slack UseAPI bool // mattermost, slack
UseLocalAvatar []string // discord
UseSASL bool // IRC UseSASL bool // IRC
UseTLS bool // IRC UseTLS bool // IRC
UseDiscriminator bool // discord UseDiscriminator bool // discord
UseFirstName bool // telegram UseFirstName bool // telegram
UseUserName bool // discord, matrix UseUserName bool // discord
UseInsecureURL bool // telegram UseInsecureURL bool // telegram
VerboseJoinPart bool // IRC VerboseJoinPart bool // IRC
WebhookBindAddress string // mattermost, slack WebhookBindAddress string // mattermost, slack
@@ -212,8 +198,6 @@ type BridgeValues struct {
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 WhatsApp map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results
Zulip map[string]Protocol Zulip map[string]Protocol
Keybase map[string]Protocol
Mumble map[string]Protocol
General Protocol General Protocol
Tengo Tengo Tengo Tengo
Gateway []Gateway Gateway []Gateway
@@ -221,9 +205,7 @@ type BridgeValues struct {
} }
type Config interface { type Config interface {
Viper() *viper.Viper
BridgeValues() *BridgeValues BridgeValues() *BridgeValues
IsKeySet(key string) bool
GetBool(key string) (bool, bool) GetBool(key string) (bool, bool)
GetInt(key string) (int, bool) GetInt(key string) (int, bool)
GetString(key string) (string, bool) GetString(key string) (string, bool)
@@ -249,17 +231,7 @@ func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
logger.Fatalf("Failed to read configuration file: %#v", err) logger.Fatalf("Failed to read configuration file: %#v", err)
} }
cfgtype := detectConfigType(cfgfile) mycfg := newConfigFromString(logger, input)
mycfg := newConfigFromString(logger, input, cfgtype)
if mycfg.cv.General.LogFile != "" {
logfile, err := os.OpenFile(mycfg.cv.General.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err == nil {
logger.Info("Opening log file ", mycfg.cv.General.LogFile)
rootLogger.Out = logfile
} else {
logger.Warn("Failed to open ", mycfg.cv.General.LogFile)
}
}
if mycfg.cv.General.MediaDownloadSize == 0 { if mycfg.cv.General.MediaDownloadSize == 0 {
mycfg.cv.General.MediaDownloadSize = 1000000 mycfg.cv.General.MediaDownloadSize = 1000000
} }
@@ -270,26 +242,14 @@ func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
return mycfg return mycfg
} }
// detectConfigType detects JSON and YAML formats, defaults to TOML.
func detectConfigType(cfgfile string) string {
fileExt := filepath.Ext(cfgfile)
switch fileExt {
case ".json":
return "json"
case ".yaml", ".yml":
return "yaml"
}
return "toml"
}
// NewConfigFromString instantiates a new configuration based on the specified string. // NewConfigFromString instantiates a new configuration based on the specified string.
func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config { func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config {
logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"}) logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
return newConfigFromString(logger, input, "toml") return newConfigFromString(logger, input)
} }
func newConfigFromString(logger *logrus.Entry, input []byte, cfgtype string) *config { func newConfigFromString(logger *logrus.Entry, input []byte) *config {
viper.SetConfigType(cfgtype) viper.SetConfigType("toml")
viper.SetEnvPrefix("matterbridge") viper.SetEnvPrefix("matterbridge")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv() viper.AutomaticEnv()
@@ -313,16 +273,6 @@ func (c *config) BridgeValues() *BridgeValues {
return c.cv return c.cv
} }
func (c *config) Viper() *viper.Viper {
return c.v
}
func (c *config) IsKeySet(key string) bool {
c.RLock()
defer c.RUnlock()
return c.v.IsSet(key)
}
func (c *config) GetBool(key string) (bool, bool) { func (c *config) GetBool(key string) (bool, bool) {
c.RLock() c.RLock()
defer c.RUnlock() defer c.RUnlock()
@@ -382,11 +332,6 @@ type TestConfig struct {
Overrides map[string]interface{} Overrides map[string]interface{}
} }
func (c *TestConfig) IsKeySet(key string) bool {
_, ok := c.Overrides[key]
return ok || c.Config.IsKeySet(key)
}
func (c *TestConfig) GetBool(key string) (bool, bool) { func (c *TestConfig) GetBool(key string) (bool, bool) {
val, ok := c.Overrides[key] val, ok := c.Overrides[key]
if ok { if ok {

View File

@@ -10,7 +10,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/discordgo" "github.com/bwmarrin/discordgo"
) )
const MessageLength = 1950 const MessageLength = 1950
@@ -21,7 +21,7 @@ type Bdiscord struct {
c *discordgo.Session c *discordgo.Session
nick string nick string
userID string useChannelID bool
guildID string guildID string
webhookID string webhookID string
webhookToken string webhookToken string
@@ -72,7 +72,6 @@ func (b *Bdiscord) Connect() error {
} }
b.Log.Info("Connection succeeded") b.Log.Info("Connection succeeded")
b.c.AddHandler(b.messageCreate) b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.messageTyping)
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)
@@ -93,7 +92,6 @@ func (b *Bdiscord) Connect() error {
} }
serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1) serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
b.nick = userinfo.Username b.nick = userinfo.Username
b.userID = userinfo.ID
b.channelsMutex.Lock() b.channelsMutex.Lock()
for _, guild := range guilds { for _, guild := range guilds {
if guild.Name == serverName || guild.ID == serverName { if guild.Name == serverName || guild.ID == serverName {
@@ -116,42 +114,31 @@ func (b *Bdiscord) Connect() error {
b.Log.Infof("Server=\"%s\" # Server ID", guild.ID) b.Log.Infof("Server=\"%s\" # Server ID", guild.ID)
} }
} }
if err != nil { if err != nil {
return err return err
} }
b.channelsMutex.RLock() b.channelsMutex.RLock()
if b.GetString("WebhookURL") == "" { if b.GetString("WebhookURL") == "" {
for _, channel := range b.channels { for _, channel := range b.channels {
b.Log.Debugf("found channel %#v", channel) b.Log.Debugf("found channel %#v", channel)
} }
} else { } 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 manageWebhooks := discordgo.PermissionManageWebhooks
var channelsDenied []string if permsErr != nil || perms&manageWebhooks != manageWebhooks {
for _, info := range b.Channels { b.Log.Warnf("Can't manage webhooks in channel \"%s\"", channel.Name)
id := b.getChannelID(info.Name) // note(qaisjp): this readlocks channelsMutex
b.Log.Debugf("Verifying PermissionManageWebhooks for %s with ID %s", info.ID, id)
perms, permsErr := b.c.UserChannelPermissions(userinfo.ID, id)
if permsErr != nil {
b.Log.Warnf("Failed to check PermissionManageWebhooks in channel \"%s\": %s", info.Name, permsErr.Error())
} else if perms&manageWebhooks == manageWebhooks {
continue
}
channelsDenied = append(channelsDenied, fmt.Sprintf("%#v", info.Name))
}
b.canEditWebhooks = len(channelsDenied) == 0
b.canEditWebhooks = false b.canEditWebhooks = false
b.Log.Info("Webhook editing is disabled because of ratelimit issues") }
/* }
if b.canEditWebhooks { if b.canEditWebhooks {
b.Log.Info("Can manage webhooks; will edit channel for global webhook on send") b.Log.Info("Can manage webhooks; will edit channel for global webhook on send")
} else { } else {
b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send") b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send")
b.Log.Warn("Can't manage webhooks in channels: ", strings.Join(channelsDenied, ", "))
} }
*/
} }
b.channelsMutex.RUnlock() b.channelsMutex.RUnlock()
@@ -186,6 +173,10 @@ func (b *Bdiscord) JoinChannel(channel config.ChannelInfo) error {
defer b.channelsMutex.Unlock() defer b.channelsMutex.Unlock()
b.channelInfoMap[channel.ID] = &channel b.channelInfoMap[channel.ID] = &channel
idcheck := strings.Split(channel.Name, "ID:")
if len(idcheck) > 1 {
b.useChannelID = true
}
return nil return nil
} }
@@ -197,14 +188,6 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
return "", fmt.Errorf("Could not find channelID for %v", msg.Channel) return "", fmt.Errorf("Could not find channelID for %v", msg.Channel)
} }
if msg.Event == config.EventUserTyping {
if b.GetBool("ShowUserTyping") {
err := b.c.ChannelTyping(channelID)
return "", err
}
return "", nil
}
// Make a action /me of the message // Make a action /me of the message
if msg.Event == config.EventUserAction { if msg.Event == config.EventUserAction {
msg.Text = "_" + msg.Text + "_" msg.Text = "_" + msg.Text + "_"
@@ -228,13 +211,34 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
// Use webhook to send the message // Use webhook to send the message
if wID != "" && msg.Event != config.EventMsgDelete { if wID != "" && msg.Event != config.EventMsgDelete {
// skip events // skip events
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange { if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
return "", nil return "", nil
} }
// If we are editing a message, delete the old message
if msg.ID != "" {
b.Log.Debugf("Deleting edited webhook message")
err := b.c.ChannelMessageDelete(channelID, msg.ID)
if err != nil {
b.Log.Errorf("Could not delete edited webhook message: %s", err)
}
}
b.Log.Debugf("Broadcasting using Webhook")
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
}
}
}
// skip empty messages // skip empty messages
if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) { if msg.Text == "" {
b.Log.Debugf("Skipping empty message %#v", msg)
return "", nil return "", nil
} }
@@ -244,25 +248,11 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
if len(msg.Username) > 32 { if len(msg.Username) > 32 {
msg.Username = msg.Username[0:32] msg.Username = msg.Username[0:32]
} }
if msg.ID != "" {
b.Log.Debugf("Editing webhook message")
uri := discordgo.EndpointWebhookToken(wID, wToken) + "/messages/" + msg.ID
_, err := b.c.RequestWithBucketID("PATCH", uri, discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
}, discordgo.EndpointWebhookToken("", ""))
if err == nil {
return msg.ID, nil
}
b.Log.Errorf("Could not edit webhook message: %s", err)
}
b.Log.Debugf("Broadcasting using Webhook")
// if we have a global webhook for this Discord account, and permission // if we have a global webhook for this Discord account, and permission
// to modify webhooks (previously verified), then set its channel to // to modify webhooks (previously verified), then set its channel to
// the message channel before using it. // the message channel before using it
// TODO: this isn't necessary if the last message from this webhook was
// sent to the current channel
if isGlobalWebhook && b.canEditWebhooks { if isGlobalWebhook && b.canEditWebhooks {
b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel) b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel)
_, err := b.c.WebhookEdit(wID, "", "", channelID) _, err := b.c.WebhookEdit(wID, "", "", channelID)
@@ -271,16 +261,10 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
return "", err return "", err
} }
} }
b.Log.Debugf("Processing webhook sending for message %#v", msg)
msg, err := b.webhookSend(&msg, wID, wToken) msg, err := b.webhookSend(&msg, wID, wToken)
if err != nil { if err != nil {
b.Log.Errorf("Could not broadcast via webook for message %#v: %s", msg, err)
return "", err return "", err
} }
if msg == nil {
return "", nil
}
return msg.ID, nil return msg.ID, nil
} }
@@ -323,7 +307,7 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return res.ID, nil return res.ID, err
} }
// useWebhook returns true if we have a webhook defined somewhere // useWebhook returns true if we have a webhook defined somewhere
@@ -392,29 +376,10 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
// uploads (from slack, telegram or mattermost). // uploads (from slack, telegram or mattermost).
// Returns messageID and error. // Returns messageID and error.
func (b *Bdiscord) webhookSend(msg *config.Message, webhookID, token string) (*discordgo.Message, error) { func (b *Bdiscord) webhookSend(msg *config.Message, webhookID, token string) (*discordgo.Message, error) {
var ( var err error
res *discordgo.Message
err error
)
// If avatar is unset, check if UseLocalAvatar contains the message's
// account or protocol, and if so, try to find a local avatar
if msg.Avatar == "" {
for _, val := range b.GetStringSlice("UseLocalAvatar") {
if msg.Protocol == val || msg.Account == val {
if avatar := b.findAvatar(msg); avatar != "" {
msg.Avatar = avatar
}
break
}
}
}
// WebhookParams can have either `Content` or `File`. // WebhookParams can have either `Content` or `File`.
res, err := b.c.WebhookExecute(
// We can't send empty messages.
if msg.Text != "" {
res, err = b.c.WebhookExecute(
webhookID, webhookID,
token, token,
true, true,
@@ -425,8 +390,7 @@ func (b *Bdiscord) webhookSend(msg *config.Message, webhookID, token string) (*d
}, },
) )
if err != nil { if err != nil {
b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err) return nil, err
}
} }
if msg.Extra != nil { if msg.Extra != nil {
@@ -437,11 +401,7 @@ func (b *Bdiscord) webhookSend(msg *config.Message, webhookID, token string) (*d
ContentType: "", ContentType: "",
Reader: bytes.NewReader(*fi.Data), Reader: bytes.NewReader(*fi.Data),
} }
content := "" _, err := b.c.WebhookExecute(
if msg.Text == "" {
content = fi.Comment
}
_, e2 := b.c.WebhookExecute(
webhookID, webhookID,
token, token,
false, false,
@@ -449,21 +409,13 @@ func (b *Bdiscord) webhookSend(msg *config.Message, webhookID, token string) (*d
Username: msg.Username, Username: msg.Username,
AvatarURL: msg.Avatar, AvatarURL: msg.Avatar,
File: &file, File: &file,
Content: content,
}, },
) )
if e2 != nil { if err != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2) return nil, fmt.Errorf("file upload failed: %s", err)
} }
} }
} }
return res, err
}
func (b *Bdiscord) findAvatar(m *config.Message) string { return res, nil
member, err := b.getGuildMemberByNick(m.Username)
if err != nil {
return ""
}
return member.User.AvatarURL("")
} }

View File

@@ -2,13 +2,15 @@ package bdiscord
import ( import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/matterbridge/discordgo" "github.com/bwmarrin/discordgo"
) )
func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam 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 := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
rmsg.Channel = b.getChannelName(m.ChannelID) 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("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg) b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
@@ -22,7 +24,11 @@ func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageD
ID: msgID, ID: msgID,
Event: config.EventMsgDelete, Event: config.EventMsgDelete,
Text: config.EventMsgDelete, Text: config.EventMsgDelete,
Channel: b.getChannelName(m.ChannelID), Channel: "ID:" + m.ChannelID,
}
if !b.useChannelID {
rmsg.Channel = b.getChannelName(m.ChannelID)
} }
b.Log.Debugf("<= Sending message from %s to gateway", b.Account) b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
@@ -31,21 +37,6 @@ func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageD
} }
} }
func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) {
if !b.GetBool("ShowUserTyping") {
return
}
// Ignore our own typing messages
if m.UserID == b.userID {
return
}
rmsg := config.Message{Account: b.Account, Event: config.EventUserTyping}
rmsg.Channel = b.getChannelName(m.ChannelID)
b.Remote <- rmsg
}
func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam
if b.GetBool("EditDisable") { if b.GetBool("EditDisable") {
return return
@@ -54,10 +45,7 @@ func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdat
if m.Message.EditedTimestamp != "" { if m.Message.EditedTimestamp != "" {
b.Log.Debugf("Sending edit message") b.Log.Debugf("Sending edit message")
m.Content += b.GetString("EditSuffix") m.Content += b.GetString("EditSuffix")
msg := &discordgo.MessageCreate{ b.messageCreate(s, (*discordgo.MessageCreate)(m))
Message: m.Message,
}
b.messageCreate(s, msg)
} }
} }
@@ -84,6 +72,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
if m.Content != "" { if m.Content != "" {
b.Log.Debugf("== Receiving event %#v", m.Message) b.Log.Debugf("== Receiving event %#v", m.Message)
m.Message.Content = b.stripCustomoji(m.Message.Content)
m.Message.Content = b.replaceChannelMentions(m.Message.Content) m.Message.Content = b.replaceChannelMentions(m.Message.Content)
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c) rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
if err != nil { if err != nil {
@@ -94,13 +83,16 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
// set channel name // set channel name
rmsg.Channel = b.getChannelName(m.ChannelID) rmsg.Channel = b.getChannelName(m.ChannelID)
if b.useChannelID {
rmsg.Channel = "ID:" + m.ChannelID
}
fromWebhook := m.WebhookID != "" // set username
if !fromWebhook && !b.GetBool("UseUserName") { if !b.GetBool("UseUserName") {
rmsg.Username = b.getNick(m.Author, m.GuildID) rmsg.Username = b.getNick(m.Author)
} else { } else {
rmsg.Username = m.Author.Username rmsg.Username = m.Author.Username
if !fromWebhook && b.GetBool("UseDiscriminator") { if b.GetBool("UseDiscriminator") {
rmsg.Username += "#" + m.Author.Discriminator rmsg.Username += "#" + m.Author.Discriminator
} }
} }
@@ -108,7 +100,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
// if we have embedded content add it to text // if we have embedded content add it to text
if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil { if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil {
for _, embed := range m.Message.Embeds { for _, embed := range m.Message.Embeds {
rmsg.Text += handleEmbed(embed) rmsg.Text = rmsg.Text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n"
} }
} }
@@ -124,9 +116,6 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
rmsg.Event = config.EventUserAction rmsg.Event = config.EventUserAction
} }
// Replace emotes
rmsg.Text = replaceEmotes(rmsg.Text)
b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account) b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg) b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
@@ -200,33 +189,3 @@ func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRe
b.Log.Debugf("<= Message is %#v", rmsg) b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
} }
func handleEmbed(embed *discordgo.MessageEmbed) string {
var t []string
var result string
t = append(t, embed.Title)
t = append(t, embed.Description)
t = append(t, embed.URL)
i := 0
for _, e := range t {
if e == "" {
continue
}
i++
if i == 1 {
result += " embed: " + e
continue
}
result += " - " + e
}
if result != "" {
result += "\n"
}
return result
}

View File

@@ -1,58 +0,0 @@
package bdiscord
import (
"testing"
"github.com/matterbridge/discordgo"
"github.com/stretchr/testify/assert"
)
func TestHandleEmbed(t *testing.T) {
testcases := map[string]struct {
embed *discordgo.MessageEmbed
result string
}{
"allempty": {
embed: &discordgo.MessageEmbed{},
result: "",
},
"one": {
embed: &discordgo.MessageEmbed{
Title: "blah",
},
result: " embed: blah\n",
},
"two": {
embed: &discordgo.MessageEmbed{
Title: "blah",
Description: "blah2",
},
result: " embed: blah - blah2\n",
},
"three": {
embed: &discordgo.MessageEmbed{
Title: "blah",
Description: "blah2",
URL: "blah3",
},
result: " embed: blah - blah2 - blah3\n",
},
"twob": {
embed: &discordgo.MessageEmbed{
Description: "blah2",
URL: "blah3",
},
result: " embed: blah2 - blah3\n",
},
"oneb": {
embed: &discordgo.MessageEmbed{
URL: "blah3",
},
result: " embed: blah3\n",
},
}
for name, tc := range testcases {
assert.Equalf(t, tc.result, handleEmbed(tc.embed), "Testcases %s", name)
}
}

View File

@@ -6,10 +6,10 @@ import (
"strings" "strings"
"unicode" "unicode"
"github.com/matterbridge/discordgo" "github.com/bwmarrin/discordgo"
) )
func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string { func (b *Bdiscord) getNick(user *discordgo.User) string {
b.membersMutex.RLock() b.membersMutex.RLock()
defer b.membersMutex.RUnlock() defer b.membersMutex.RUnlock()
@@ -23,9 +23,9 @@ func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
} }
// If we didn't find nick, search for it. // If we didn't find nick, search for it.
member, err := b.c.GuildMember(guildID, user.ID) member, err := b.c.GuildMember(b.guildID, user.ID)
if err != nil { if err != nil {
b.Log.Warnf("Failed to fetch information for member %#v on guild %#v: %s", user, guildID, err) b.Log.Warnf("Failed to fetch information for member %#v: %s", user, err)
return user.Username return user.Username
} else if member == nil { } else if member == nil {
b.Log.Warnf("Got no information for member %#v", user) b.Log.Warnf("Got no information for member %#v", user)
@@ -96,13 +96,6 @@ func (b *Bdiscord) getChannelName(id string) string {
b.channelsMutex.RLock() b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock() defer b.channelsMutex.RUnlock()
for _, c := range b.channelInfoMap {
if c.Name == "ID:"+id {
// if we have ID: specified in our gateway configuration return this
return c.Name
}
}
for _, channel := range b.channels { for _, channel := range b.channels {
if channel.ID == id { if channel.ID == id {
return b.getCategoryChannelName(channel.Name, channel.ParentID) return b.getCategoryChannelName(channel.Name, channel.ParentID)
@@ -136,8 +129,8 @@ func (b *Bdiscord) getCategoryChannelName(name, parentID string) string {
var ( var (
// See https://discordapp.com/developers/docs/reference#message-formatting. // See https://discordapp.com/developers/docs/reference#message-formatting.
channelMentionRE = regexp.MustCompile("<#[0-9]+>") channelMentionRE = regexp.MustCompile("<#[0-9]+>")
emojiRE = regexp.MustCompile("<(:.*?:)[0-9]+>")
userMentionRE = regexp.MustCompile("@[^@\n]{1,32}") userMentionRE = regexp.MustCompile("@[^@\n]{1,32}")
emoteRE = regexp.MustCompile(`<a?(:\w+:)\d+>`)
) )
func (b *Bdiscord) replaceChannelMentions(text string) string { func (b *Bdiscord) replaceChannelMentions(text string) string {
@@ -183,14 +176,13 @@ func (b *Bdiscord) replaceUserMentions(text string) string {
return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc) return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc)
} }
func replaceEmotes(text string) string { func (b *Bdiscord) stripCustomoji(text string) string {
return emoteRE.ReplaceAllString(text, "$1") return emojiRE.ReplaceAllString(text, `$1`)
} }
func (b *Bdiscord) replaceAction(text string) (string, bool) { func (b *Bdiscord) replaceAction(text string) (string, bool) {
length := len(text) if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") {
if length > 1 && text[0] == '_' && text[length-1] == '_' { return text[1 : len(text)-1], true
return text[1 : length-1], true
} }
return text, false return text, false
} }

View File

@@ -5,10 +5,7 @@ import (
"fmt" "fmt"
"image/png" "image/png"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os"
"os/exec"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@@ -17,10 +14,8 @@ import (
"golang.org/x/image/webp" "golang.org/x/image/webp"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gitlab.com/golang-commonmark/markdown"
) )
// DownloadFile downloads the given non-authenticated URL. // DownloadFile downloads the given non-authenticated URL.
@@ -181,21 +176,15 @@ func ClipMessage(text string, length int) string {
return text return text
} }
// ParseMarkdown takes in an input string as markdown and parses it to html
func ParseMarkdown(input string) string { func ParseMarkdown(input string) string {
extensions := parser.HardLineBreak | parser.NoIntraEmphasis md := markdown.New(markdown.XHTMLOutput(true), markdown.Breaks(true))
markdownParser := parser.NewWithExtensions(extensions) res := md.RenderToString([]byte(input))
renderer := html.NewRenderer(html.RendererOptions{
Flags: 0,
})
parsedMarkdown := markdown.ToHTML([]byte(input), markdownParser, renderer)
res := string(parsedMarkdown)
res = strings.TrimPrefix(res, "<p>") res = strings.TrimPrefix(res, "<p>")
res = strings.TrimSuffix(res, "</p>\n") res = strings.TrimSuffix(res, "</p>\n")
return res return res
} }
// ConvertWebPToPNG converts input data (which should be WebP format) to PNG format // ConvertWebPToPNG convert input data (which should be WebP format to PNG format)
func ConvertWebPToPNG(data *[]byte) error { func ConvertWebPToPNG(data *[]byte) error {
r := bytes.NewReader(*data) r := bytes.NewReader(*data)
m, err := webp.Decode(r) m, err := webp.Decode(r)
@@ -210,49 +199,3 @@ func ConvertWebPToPNG(data *[]byte) error {
*data = w.Bytes() *data = w.Bytes()
return nil return nil
} }
// CanConvertTgsToX Checks whether the external command necessary for ConvertTgsToX works.
func CanConvertTgsToX() error {
// We depend on the fact that `lottie_convert.py --help` has exit status 0.
// Hyrum's Law predicted this, and Murphy's Law predicts that this will break eventually.
// However, there is no alternative like `lottie_convert.py --is-properly-installed`
cmd := exec.Command("lottie_convert.py", "--help")
return cmd.Run()
}
// ConvertTgsToWebP convert input data (which should be tgs format) to WebP format
// This relies on an external command, which is ugly, but works.
func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
// lottie can't handle input from a pipe, so write to a temporary file:
tmpFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-*.tgs")
if err != nil {
return err
}
tmpFileName := tmpFile.Name()
defer func() {
if removeErr := os.Remove(tmpFileName); removeErr != nil {
logger.Errorf("Could not delete temporary file %s: %v", tmpFileName, removeErr)
}
}()
if _, writeErr := tmpFile.Write(*data); writeErr != nil {
return writeErr
}
// Must close before calling lottie to avoid data races:
if closeErr := tmpFile.Close(); closeErr != nil {
return closeErr
}
// Call lottie to transform:
cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpFileName, "/dev/stdout")
cmd.Stderr = nil
// NB: lottie writes progress into to stderr in all cases.
stdout, stderr := cmd.Output()
if stderr != nil {
// 'stderr' already contains some parts of Stderr, because it was set to 'nil'.
return stderr
}
*data = stdout
return nil
}

View File

@@ -10,8 +10,8 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/dfordsoft/golib/ic"
"github.com/lrstanley/girc" "github.com/lrstanley/girc"
"github.com/missdeer/golib/ic"
"github.com/paulrosania/go-charset/charset" "github.com/paulrosania/go-charset/charset"
"github.com/saintfish/chardet" "github.com/saintfish/chardet"

View File

@@ -4,7 +4,6 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"hash/crc32" "hash/crc32"
"io/ioutil"
"net" "net"
"sort" "sort"
"strconv" "strconv"
@@ -15,7 +14,6 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/lrstanley/girc" "github.com/lrstanley/girc"
stripmd "github.com/writeas/go-strip-markdown"
// We need to import the 'data' package as an implicit dependency. // We need to import the 'data' package as an implicit dependency.
// See: https://godoc.org/github.com/paulrosania/go-charset/charset // See: https://godoc.org/github.com/paulrosania/go-charset/charset
@@ -158,10 +156,6 @@ func (b *Birc) Send(msg config.Message) (string, error) {
} }
var msgLines []string var msgLines []string
if b.GetBool("StripMarkdown") {
msg.Text = stripmd.Strip(msg.Text)
}
if b.GetBool("MessageSplit") { if b.GetBool("MessageSplit") {
msgLines = helper.GetSubLines(msg.Text, b.MessageLength) msgLines = helper.GetSubLines(msg.Text, b.MessageLength)
} else { } else {
@@ -173,8 +167,12 @@ func (b *Birc) Send(msg config.Message) (string, error) {
return "", nil return "", nil
} }
msg.Text = msgLines[i] b.Local <- config.Message{
b.Local <- msg Text: msgLines[i],
Username: msg.Username,
Channel: msg.Channel,
Event: msg.Event,
}
} }
return "", nil return "", nil
} }
@@ -207,7 +205,7 @@ func (b *Birc) doSend() {
for msg := range b.Local { for msg := range b.Local {
<-throttle.C <-throttle.C
username := msg.Username username := msg.Username
if b.GetBool("Colornicks") && len(username) > 1 { if b.GetBool("Colornicks") {
checksum := crc32.ChecksumIEEE([]byte(msg.Username)) checksum := crc32.ChecksumIEEE([]byte(msg.Username))
colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
@@ -241,18 +239,6 @@ func (b *Birc) getClient() (*girc.Client, error) {
user = user[1:] user = user[1:]
} }
debug := ioutil.Discard
if b.GetInt("DebugLevel") == 2 {
debug = b.Log.Writer()
}
pingDelay, err := time.ParseDuration(b.GetString("pingdelay"))
if err != nil || pingDelay == 0 {
pingDelay = time.Minute
}
b.Log.Debugf("setting pingdelay to %s", pingDelay)
i := girc.New(girc.Config{ i := girc.New(girc.Config{
Server: server, Server: server,
ServerPass: b.GetString("Password"), ServerPass: b.GetString("Password"),
@@ -262,10 +248,7 @@ func (b *Birc) getClient() (*girc.Client, error) {
Name: b.GetString("Nick"), Name: b.GetString("Nick"),
SSL: b.GetBool("UseTLS"), SSL: b.GetBool("UseTLS"),
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec
PingDelay: pingDelay, PingDelay: time.Minute,
// skip gIRC internal rate limiting, since we have our own throttling
AllowFlood: true,
Debug: debug,
}) })
return i, nil return i, nil
} }

View File

@@ -1,59 +0,0 @@
package bkeybase
import (
"strconv"
"github.com/42wim/matterbridge/bridge/config"
"github.com/keybase/go-keybase-chat-bot/kbchat/types/chat1"
)
func (b *Bkeybase) handleKeybase() {
sub, err := b.kbc.ListenForNewTextMessages()
if err != nil {
b.Log.Errorf("Error listening: %s", err.Error())
}
go func() {
for {
msg, err := sub.Read()
if err != nil {
b.Log.Errorf("failed to read message: %s", err.Error())
}
if msg.Message.Content.TypeName != "text" {
continue
}
if msg.Message.Sender.Username == b.kbc.GetUsername() {
continue
}
b.handleMessage(msg.Message)
}
}()
}
func (b *Bkeybase) handleMessage(msg chat1.MsgSummary) {
b.Log.Debugf("== Receiving event: %#v", msg)
if msg.Channel.TopicName != b.channel || msg.Channel.Name != b.team {
return
}
if msg.Sender.Username != b.kbc.GetUsername() {
// TODO download avatar
// Create our message
rmsg := config.Message{Username: msg.Sender.Username, Text: msg.Content.Text.Body, UserID: string(msg.Sender.Uid), Channel: msg.Channel.TopicName, ID: strconv.Itoa(int(msg.Id)), Account: b.Account}
// Text must be a string
if msg.Content.TypeName != "text" {
b.Log.Errorf("message is not text")
return
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", msg.Sender.Username, msg.Channel.Name)
b.Remote <- rmsg
}
}

View File

@@ -1,106 +0,0 @@
package bkeybase
import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/keybase/go-keybase-chat-bot/kbchat"
)
// Bkeybase bridge structure
type Bkeybase struct {
kbc *kbchat.API
user string
channel string
team string
*bridge.Config
}
// New initializes Bkeybase object and sets team
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bkeybase{Config: cfg}
b.team = b.Config.GetString("Team")
return b
}
// Connect starts keybase API and listener loop
func (b *Bkeybase) Connect() error {
var err error
b.Log.Infof("Connecting %s", b.GetString("Team"))
// use default keybase location (`keybase`)
b.kbc, err = kbchat.Start(kbchat.RunOptions{})
if err != nil {
return err
}
b.user = b.kbc.GetUsername()
b.Log.Info("Connection succeeded")
go b.handleKeybase()
return nil
}
// Disconnect doesn't do anything for now
func (b *Bkeybase) Disconnect() error {
return nil
}
// JoinChannel sets channel name in struct
func (b *Bkeybase) JoinChannel(channel config.ChannelInfo) error {
if _, err := b.kbc.JoinChannel(b.team, channel.Name); err != nil {
return err
}
b.channel = channel.Name
return nil
}
// Send receives bridge messages and sends them to Keybase chat room
func (b *Bkeybase) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
// Handle /me events
if msg.Event == config.EventUserAction {
msg.Text = "_" + msg.Text + "_"
}
// Delete message if we have an ID
// Delete message not supported by keybase go library yet
// Edit message if we have an ID
// kbchat lib does not support message editing yet
if len(msg.Extra["file"]) > 0 {
// Upload a file
dir, err := ioutil.TempDir("", "matterbridge")
if err != nil {
return "", err
}
defer os.RemoveAll(dir)
for _, f := range msg.Extra["file"] {
fname := f.(config.FileInfo).Name
fdata := *f.(config.FileInfo).Data
fcaption := f.(config.FileInfo).Comment
fpath := filepath.Join(dir, fname)
if err = ioutil.WriteFile(fpath, fdata, 0600); err != nil {
return "", err
}
_, _ = b.kbc.SendAttachmentByTeam(b.team, &b.channel, fpath, fcaption)
}
return "", nil
}
// Send regular message
text := msg.Username + msg.Text
resp, err := b.kbc.SendMessageByTeamName(b.team, &b.channel, text)
if err != nil {
return "", err
}
return strconv.Itoa(int(*resp.Result.MessageID)), err
}

View File

@@ -1,166 +0,0 @@
package bmatrix
import (
"encoding/json"
"errors"
"html"
"strings"
"time"
matrix "github.com/matrix-org/gomatrix"
)
func newMatrixUsername(username string) *matrixUsername {
mUsername := new(matrixUsername)
// check if we have a </tag>. if we have, we don't escape HTML. #696
if htmlTag.MatchString(username) {
mUsername.formatted = username
// remove the HTML formatting for beautiful push messages #1188
mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "")
} else {
mUsername.formatted = html.EscapeString(username)
mUsername.plain = username
}
return mUsername
}
// getRoomID retrieves a matching room ID from the channel name.
func (b *Bmatrix) getRoomID(channel string) string {
b.RLock()
defer b.RUnlock()
for ID, name := range b.RoomMap {
if name == channel {
return ID
}
}
return ""
}
// interface2Struct marshals and immediately unmarshals an interface.
// Useful for converting map[string]interface{} to a struct.
func interface2Struct(in interface{}, out interface{}) error {
jsonObj, err := json.Marshal(in)
if err != nil {
return err //nolint:wrapcheck
}
return json.Unmarshal(jsonObj, out)
}
// getDisplayName retrieves the displayName for mxid, querying the homserver if the mxid is not in the cache.
func (b *Bmatrix) getDisplayName(mxid string) string {
if b.GetBool("UseUserName") {
return mxid[1:]
}
b.RLock()
if val, present := b.NicknameMap[mxid]; present {
b.RUnlock()
return val.displayName
}
b.RUnlock()
displayName, err := b.mc.GetDisplayName(mxid)
var httpError *matrix.HTTPError
if errors.As(err, &httpError) {
b.Log.Warnf("Couldn't retrieve the display name for %s", mxid)
}
if err != nil {
return b.cacheDisplayName(mxid, mxid[1:])
}
return b.cacheDisplayName(mxid, displayName.DisplayName)
}
// cacheDisplayName stores the mapping between a mxid and a display name, to be reused later without performing a query to the homserver.
// Note that old entries are cleaned when this function is called.
func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
now := time.Now()
// scan to delete old entries, to stop memory usage from becoming too high with old entries
toDelete := []string{}
b.RLock()
for k, v := range b.NicknameMap {
if now.Sub(v.lastUpdated) > 10*time.Minute {
toDelete = append(toDelete, k)
}
}
b.RUnlock()
b.Lock()
for _, v := range toDelete {
delete(b.NicknameMap, v)
}
b.NicknameMap[mxid] = NicknameCacheEntry{
displayName: displayName,
lastUpdated: now,
}
b.Unlock()
return displayName
}
// handleError converts errors into httpError.
//nolint:exhaustivestruct
func handleError(err error) *httpError {
var mErr matrix.HTTPError
if !errors.As(err, &mErr) {
return &httpError{
Err: "not a HTTPError",
}
}
var httpErr httpError
if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil {
return &httpError{
Err: "unmarshal failed",
}
}
return &httpErr
}
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
}
// getAvatarURL returns the avatar URL of the specified sender.
func (b *Bmatrix) getAvatarURL(sender string) string {
urlPath := b.mc.BuildURL("profile", sender, "avatar_url")
s := struct {
AvatarURL string `json:"avatar_url"`
}{}
err := b.mc.MakeRequest("GET", urlPath, nil, &s)
if err != nil {
b.Log.Errorf("getAvatarURL failed: %s", err)
return ""
}
url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/")
if url != "" {
url += "?width=37&height=37&method=crop"
}
return url
}

View File

@@ -3,71 +3,31 @@ package bmatrix
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"html"
"mime" "mime"
"regexp" "regexp"
"strings" "strings"
"sync" "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/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
matrix "github.com/matrix-org/gomatrix" matrix "github.com/matterbridge/gomatrix"
) )
var (
htmlTag = regexp.MustCompile("</.*?>")
htmlReplacementTag = regexp.MustCompile("<[^>]*>")
)
type NicknameCacheEntry struct {
displayName string
lastUpdated time.Time
}
type Bmatrix struct { type Bmatrix struct {
mc *matrix.Client mc *matrix.Client
UserID string UserID string
NicknameMap map[string]NicknameCacheEntry
RoomMap map[string]string RoomMap map[string]string
sync.RWMutex sync.RWMutex
htmlTag *regexp.Regexp
*bridge.Config *bridge.Config
} }
type httpError struct {
Errcode string `json:"errcode"`
Err string `json:"error"`
RetryAfterMs int `json:"retry_after_ms"`
}
type matrixUsername struct {
plain string
formatted string
}
// SubTextMessage represents the new content of the message in edit messages.
type SubTextMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
}
// MessageRelation explains how the current message relates to a previous message.
// Notably used for message edits.
type MessageRelation struct {
EventID string `json:"event_id"`
Type string `json:"rel_type"`
}
type EditedMessage struct {
NewContent SubTextMessage `json:"m.new_content"`
RelatedTo MessageRelation `json:"m.relates_to"`
matrix.TextMessage
}
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
b := &Bmatrix{Config: cfg} b := &Bmatrix{Config: cfg}
b.htmlTag = regexp.MustCompile("</.*?>")
b.RoomMap = make(map[string]string) b.RoomMap = make(map[string]string)
b.NicknameMap = make(map[string]NicknameCacheEntry)
return b return b
} }
@@ -82,7 +42,6 @@ func (b *Bmatrix) Connect() error {
Type: "m.login.password", Type: "m.login.password",
User: b.GetString("Login"), User: b.GetString("Login"),
Password: b.GetString("Password"), Password: b.GetString("Password"),
Identifier: matrix.NewUserIdentifier(b.GetString("Login")),
}) })
if err != nil { if err != nil {
return err return err
@@ -99,25 +58,14 @@ func (b *Bmatrix) Disconnect() error {
} }
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error { func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
retry:
resp, err := b.mc.JoinRoom(channel.Name, "", nil) resp, err := b.mc.JoinRoom(channel.Name, "", nil)
if err != nil { if err != nil {
httpErr := handleError(err)
if httpErr.Errcode == "M_LIMIT_EXCEEDED" {
b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before joining %s", httpErr.RetryAfterMs/1000, channel.Name)
time.Sleep((time.Duration(httpErr.RetryAfterMs) * time.Millisecond))
goto retry
}
return err return err
} }
b.Lock() b.Lock()
b.RoomMap[resp.RoomID] = channel.Name b.RoomMap[resp.RoomID] = channel.Name
b.Unlock() b.Unlock()
return err
return nil
} }
func (b *Bmatrix) Send(msg config.Message) (string, error) { func (b *Bmatrix) Send(msg config.Message) (string, error) {
@@ -126,14 +74,11 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
channel := b.getRoomID(msg.Channel) channel := b.getRoomID(msg.Channel)
b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel) b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel)
username := newMatrixUsername(msg.Username)
// Make a action /me of the message // Make a action /me of the message
if msg.Event == config.EventUserAction { if msg.Event == config.EventUserAction {
m := matrix.TextMessage{ m := matrix.TextMessage{
MsgType: "m.emote", MsgType: "m.emote",
Body: username.plain + msg.Text, Body: msg.Username + msg.Text,
FormattedBody: username.formatted + msg.Text,
} }
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m) resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
if err != nil { if err != nil {
@@ -168,68 +113,45 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
} }
// Edit message if we have an ID // Edit message if we have an ID
if msg.ID != "" { // matrix has no editing support
rmsg := EditedMessage{TextMessage: matrix.TextMessage{
Body: username.plain + msg.Text,
MsgType: "m.text",
}}
if b.GetBool("HTMLDisable") {
rmsg.TextMessage.FormattedBody = username.formatted + "* " + msg.Text
} else {
rmsg.Format = "org.matrix.custom.html"
rmsg.TextMessage.FormattedBody = username.formatted + "* " + helper.ParseMarkdown(msg.Text)
}
rmsg.NewContent = SubTextMessage{
Body: rmsg.TextMessage.Body,
MsgType: "m.text",
}
rmsg.RelatedTo = MessageRelation{
EventID: msg.ID,
Type: "m.replace",
}
_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
if err != nil {
return "", err
}
return msg.ID, nil
}
// Use notices to send join/leave events // Use notices to send join/leave events
if msg.Event == config.EventJoinLeave { if msg.Event == config.EventJoinLeave {
m := matrix.TextMessage{ resp, err := b.mc.SendNotice(channel, msg.Username+msg.Text)
MsgType: "m.notice",
Body: username.plain + msg.Text,
FormattedBody: username.formatted + msg.Text,
}
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
if err != nil { if err != nil {
return "", err return "", err
} }
return resp.EventID, err return resp.EventID, err
} }
if b.GetBool("HTMLDisable") { username := html.EscapeString(msg.Username)
resp, err := b.mc.SendText(channel, username.plain+msg.Text) // check if we have a </tag>. if we have, we don't escape HTML. #696
if err != nil { if b.htmlTag.MatchString(msg.Username) {
return "", err username = msg.Username
} }
return resp.EventID, err
}
// Post normal message with HTML support (eg riot.im) // Post normal message with HTML support (eg riot.im)
resp, err := b.mc.SendFormattedText(channel, username.plain+msg.Text, username.formatted+helper.ParseMarkdown(msg.Text)) resp, err := b.mc.SendHTML(channel, msg.Username+msg.Text, username+helper.ParseMarkdown(msg.Text))
if err != nil { if err != nil {
return "", err return "", err
} }
return resp.EventID, err return resp.EventID, err
} }
func (b *Bmatrix) getRoomID(channel string) string {
b.RLock()
defer b.RUnlock()
for ID, name := range b.RoomMap {
if name == channel {
return ID
}
}
return ""
}
func (b *Bmatrix) handlematrix() { 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.redaction", b.handleEvent)
syncer.OnEventType("m.room.message", b.handleEvent) syncer.OnEventType("m.room.message", b.handleEvent)
syncer.OnEventType("m.room.member", b.handleMemberChange)
go func() { go func() {
for { for {
if err := b.mc.Sync(); err != nil { if err := b.mc.Sync(); err != nil {
@@ -239,45 +161,6 @@ func (b *Bmatrix) handlematrix() {
}() }()
} }
func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool {
relationInterface, present := ev.Content["m.relates_to"]
newContentInterface, present2 := ev.Content["m.new_content"]
if !(present && present2) {
return false
}
var relation MessageRelation
if err := interface2Struct(relationInterface, &relation); err != nil {
b.Log.Warnf("Couldn't parse 'm.relates_to' object with value %#v", relationInterface)
return false
}
var newContent SubTextMessage
if err := interface2Struct(newContentInterface, &newContent); err != nil {
b.Log.Warnf("Couldn't parse 'm.new_content' object with value %#v", newContentInterface)
return false
}
if relation.Type != "m.replace" {
return false
}
rmsg.ID = relation.EventID
rmsg.Text = newContent.Body
b.Remote <- rmsg
return true
}
func (b *Bmatrix) handleMemberChange(ev *matrix.Event) {
// Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information
if ev.Content["membership"] == "join" {
if dn, ok := ev.Content["displayname"].(string); ok {
b.cacheDisplayName(ev.Sender, dn)
}
}
}
func (b *Bmatrix) handleEvent(ev *matrix.Event) { func (b *Bmatrix) handleEvent(ev *matrix.Event) {
b.Log.Debugf("== Receiving event: %#v", ev) b.Log.Debugf("== Receiving event: %#v", ev)
if ev.Sender != b.UserID { if ev.Sender != b.UserID {
@@ -289,15 +172,10 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
return return
} }
// TODO download avatar
// Create our message // Create our message
rmsg := config.Message{ rmsg := config.Message{Username: ev.Sender[1:], Channel: channel, Account: b.Account, UserID: ev.Sender, ID: ev.ID}
Username: b.getDisplayName(ev.Sender),
Channel: channel,
Account: b.Account,
UserID: ev.Sender,
ID: ev.ID,
Avatar: b.getAvatarURL(ev.Sender),
}
// Text must be a string // Text must be a string
if rmsg.Text, ok = ev.Content["body"].(string); !ok { if rmsg.Text, ok = ev.Content["body"].(string); !ok {
@@ -326,11 +204,6 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
rmsg.Event = config.EventUserAction rmsg.Event = config.EventUserAction
} }
// Is it an edit?
if b.handleEdit(ev, rmsg) {
return
}
// Do we have attachments // Do we have attachments
if b.containsAttachment(ev.Content) { if b.containsAttachment(ev.Content) {
err := b.handleDownloadFile(&rmsg, ev.Content) err := b.handleDownloadFile(&rmsg, ev.Content)
@@ -415,16 +288,24 @@ func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string
// handleUploadFile handles native upload of a file. // handleUploadFile handles native upload of a file.
func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) { func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) {
username := newMatrixUsername(msg.Username)
content := bytes.NewReader(*fi.Data) content := bytes.NewReader(*fi.Data)
sp := strings.Split(fi.Name, ".") sp := strings.Split(fi.Name, ".")
mtype := mime.TypeByExtension("." + sp[len(sp)-1]) mtype := mime.TypeByExtension("." + sp[len(sp)-1])
// image and video uploads send no username, we have to do this ourself here #715 if !strings.Contains(mtype, "image") && !strings.Contains(mtype, "video") {
_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment) return
}
if fi.Comment != "" {
_, err := b.mc.SendText(channel, msg.Username+fi.Comment)
if err != nil { if err != nil {
b.Log.Errorf("file comment failed: %#v", err) 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) b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data))) res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
if err != nil { if err != nil {
@@ -445,34 +326,22 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
if err != nil { if err != nil {
b.Log.Errorf("sendImage failed: %#v", err) b.Log.Errorf("sendImage failed: %#v", err)
} }
case strings.Contains(mtype, "audio"):
b.Log.Debugf("sendAudio %s", res.ContentURI)
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{
MsgType: "m.audio",
Body: fi.Name,
URL: res.ContentURI,
Info: matrix.AudioInfo{
Mimetype: mtype,
Size: uint(len(*fi.Data)),
},
})
if err != nil {
b.Log.Errorf("sendAudio failed: %#v", err)
}
default:
b.Log.Debugf("sendFile %s", res.ContentURI)
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{
MsgType: "m.file",
Body: fi.Name,
URL: res.ContentURI,
Info: matrix.FileInfo{
Mimetype: mtype,
Size: uint(len(*fi.Data)),
},
})
if err != nil {
b.Log.Errorf("sendFile failed: %#v", err)
}
} }
b.Log.Debugf("result: %#v", res) 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,28 +0,0 @@
package bmatrix
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPlainUsername(t *testing.T) {
uut := newMatrixUsername("MyUser")
assert.Equal(t, "MyUser", uut.formatted)
assert.Equal(t, "MyUser", uut.plain)
}
func TestHTMLUsername(t *testing.T) {
uut := newMatrixUsername("<b>MyUser</b>")
assert.Equal(t, "<b>MyUser</b>", uut.formatted)
assert.Equal(t, "MyUser", uut.plain)
}
func TestFancyUsername(t *testing.T) {
uut := newMatrixUsername("<MyUser>")
assert.Equal(t, "&lt;MyUser&gt;", uut.formatted)
assert.Equal(t, "<MyUser>", uut.plain)
}

View File

@@ -4,7 +4,7 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient" "github.com/42wim/matterbridge/matterclient"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/model"
) )
// handleDownloadAvatar downloads the avatar of userid from channel // handleDownloadAvatar downloads the avatar of userid from channel
@@ -66,10 +66,6 @@ func (b *Bmattermost) handleMatter() {
} else { } else {
b.Log.Debugf("Choosing login/password based receiving") b.Log.Debugf("Choosing login/password based receiving")
} }
// if for some reason we only want to sent stuff to mattermost but not receive, return
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") != "" && b.GetString("Token") == "" && b.GetString("Login") == "" {
b.Log.Debugf("No WebhookBindAddress specified, only WebhookURL. You will not receive messages from mattermost, only sending is possible.")
}
go b.handleMatterClient(messages) go b.handleMatterClient(messages)
} }
var ok bool var ok bool

View File

@@ -7,7 +7,7 @@ import (
"github.com/42wim/matterbridge/bridge/helper" "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/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/model"
) )
func (b *Bmattermost) doConnectWebhookBind() error { func (b *Bmattermost) doConnectWebhookBind() error {

View File

@@ -1,101 +0,0 @@
package bmsteams
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
msgraph "github.com/yaegashi/msgraph.go/beta"
)
func (b *Bmsteams) findFile(weburl string) (string, error) {
itemRB, err := b.gc.GetDriveItemByURL(b.ctx, weburl)
if err != nil {
return "", err
}
itemRB.Workbook().Worksheets()
b.gc.Workbooks()
item, err := itemRB.Request().Get(b.ctx)
if err != nil {
return "", err
}
if url, ok := item.GetAdditionalData("@microsoft.graph.downloadUrl"); ok {
return url.(string), nil
}
return "", nil
}
// handleDownloadFile handles file download
func (b *Bmsteams) handleDownloadFile(rmsg *config.Message, filename, weburl string) error {
realURL, err := b.findFile(weburl)
if err != nil {
return err
}
// Actually download the file.
data, err := helper.DownloadFile(realURL)
if err != nil {
return fmt.Errorf("download %s failed %#v", weburl, err)
}
// If a comment is attached to the file(s) it is in the 'Text' field of the teams 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, filename, comment, weburl, data, b.General)
return nil
}
func (b *Bmsteams) handleAttachments(rmsg *config.Message, msg msgraph.ChatMessage) {
for _, a := range msg.Attachments {
//remove the attachment tags from the text
rmsg.Text = attachRE.ReplaceAllString(rmsg.Text, "")
//handle a code snippet (code block)
if *a.ContentType == "application/vnd.microsoft.card.codesnippet" {
b.handleCodeSnippet(rmsg, a)
continue
}
//handle the download
err := b.handleDownloadFile(rmsg, *a.Name, *a.ContentURL)
if err != nil {
b.Log.Errorf("download of %s failed: %s", *a.Name, err)
}
}
}
type AttachContent struct {
Language string `json:"language"`
CodeSnippetURL string `json:"codeSnippetUrl"`
}
func (b *Bmsteams) handleCodeSnippet(rmsg *config.Message, attach msgraph.ChatMessageAttachment) {
var content AttachContent
err := json.Unmarshal([]byte(*attach.Content), &content)
if err != nil {
b.Log.Errorf("unmarshal codesnippet failed: %s", err)
return
}
s := strings.Split(content.CodeSnippetURL, "/")
if len(s) != 13 {
b.Log.Errorf("codesnippetUrl has unexpected size: %s", content.CodeSnippetURL)
return
}
resp, err := b.gc.Teams().Request().Client().Get(content.CodeSnippetURL)
if err != nil {
b.Log.Errorf("retrieving snippet content failed:%s", err)
return
}
defer resp.Body.Close()
res, err := ioutil.ReadAll(resp.Body)
if err != nil {
b.Log.Errorf("reading snippet data failed: %s", err)
return
}
rmsg.Text = rmsg.Text + "\n```" + content.Language + "\n" + string(res) + "\n```\n"
}

View File

@@ -1,224 +0,0 @@
package bmsteams
import (
"context"
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/davecgh/go-spew/spew"
"github.com/mattn/godown"
msgraph "github.com/yaegashi/msgraph.go/beta"
"github.com/yaegashi/msgraph.go/msauth"
"golang.org/x/oauth2"
)
var defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"}
var attachRE = regexp.MustCompile(`<attachment id=.*?attachment>`)
type Bmsteams struct {
gc *msgraph.GraphServiceRequestBuilder
ctx context.Context
botID string
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Bmsteams{Config: cfg}
}
func (b *Bmsteams) Connect() error {
tokenCachePath := b.GetString("sessionFile")
if tokenCachePath == "" {
tokenCachePath = "msteams_session.json"
}
ctx := context.Background()
m := msauth.NewManager()
m.LoadFile(tokenCachePath) //nolint:errcheck
ts, err := m.DeviceAuthorizationGrant(ctx, b.GetString("TenantID"), b.GetString("ClientID"), defaultScopes, nil)
if err != nil {
return err
}
err = m.SaveFile(tokenCachePath)
if err != nil {
b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err)
}
// make file readable only for matterbridge user
err = os.Chmod(tokenCachePath, 0600)
if err != nil {
b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err)
}
httpClient := oauth2.NewClient(ctx, ts)
graphClient := msgraph.NewClient(httpClient)
b.gc = graphClient
b.ctx = ctx
err = b.setBotID()
if err != nil {
return err
}
b.Log.Info("Connection succeeded")
return nil
}
func (b *Bmsteams) Disconnect() error {
return nil
}
func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error {
go func(name string) {
for {
err := b.poll(name)
if err != nil {
b.Log.Errorf("polling failed for %s: %s. retrying in 5 seconds", name, err)
}
time.Sleep(time.Second * 5)
}
}(channel.Name)
return nil
}
func (b *Bmsteams) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
if msg.ParentID != "" && msg.ParentID != "msg-parent-not-found" {
return b.sendReply(msg)
}
if msg.ParentID == "msg-parent-not-found" {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request()
text := msg.Username + msg.Text
content := &msgraph.ItemBody{Content: &text}
rmsg := &msgraph.ChatMessage{Body: content}
res, err := ct.Add(b.ctx, rmsg)
if err != nil {
return "", err
}
return *res.ID, nil
}
func (b *Bmsteams) sendReply(msg config.Message) (string, error) {
ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().ID(msg.ParentID).Replies().Request()
// Handle prefix hint for unthreaded messages.
text := msg.Username + msg.Text
content := &msgraph.ItemBody{Content: &text}
rmsg := &msgraph.ChatMessage{Body: content}
res, err := ct.Add(b.ctx, rmsg)
if err != nil {
return "", err
}
return *res.ID, nil
}
func (b *Bmsteams) getMessages(channel string) ([]msgraph.ChatMessage, error) {
ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(channel).Messages().Request()
rct, err := ct.Get(b.ctx)
if err != nil {
return nil, err
}
b.Log.Debugf("got %#v messages", len(rct))
return rct, nil
}
//nolint:gocognit
func (b *Bmsteams) poll(channelName string) error {
msgmap := make(map[string]time.Time)
b.Log.Debug("getting initial messages")
res, err := b.getMessages(channelName)
if err != nil {
return err
}
for _, msg := range res {
msgmap[*msg.ID] = *msg.CreatedDateTime
if msg.LastModifiedDateTime != nil {
msgmap[*msg.ID] = *msg.LastModifiedDateTime
}
}
time.Sleep(time.Second * 5)
b.Log.Debug("polling for messages")
for {
res, err := b.getMessages(channelName)
if err != nil {
return err
}
for i := len(res) - 1; i >= 0; i-- {
msg := res[i]
if mtime, ok := msgmap[*msg.ID]; ok {
if mtime == *msg.CreatedDateTime && msg.LastModifiedDateTime == nil {
continue
}
if msg.LastModifiedDateTime != nil && mtime == *msg.LastModifiedDateTime {
continue
}
}
if b.GetBool("debug") {
b.Log.Debug("Msg dump: ", spew.Sdump(msg))
}
// skip non-user message for now.
if msg.From.User == nil {
continue
}
if *msg.From.User.ID == b.botID {
b.Log.Debug("skipping own message")
msgmap[*msg.ID] = *msg.CreatedDateTime
continue
}
msgmap[*msg.ID] = *msg.CreatedDateTime
if msg.LastModifiedDateTime != nil {
msgmap[*msg.ID] = *msg.LastModifiedDateTime
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", *msg.From.User.DisplayName, b.Account)
text := b.convertToMD(*msg.Body.Content)
rmsg := config.Message{
Username: *msg.From.User.DisplayName,
Text: text,
Channel: channelName,
Account: b.Account,
Avatar: "",
UserID: *msg.From.User.ID,
ID: *msg.ID,
Extra: make(map[string][]interface{}),
}
b.handleAttachments(&rmsg, msg)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
time.Sleep(time.Second * 5)
}
}
func (b *Bmsteams) setBotID() error {
req := b.gc.Me().Request()
r, err := req.Get(b.ctx)
if err != nil {
return err
}
b.botID = *r.ID
return nil
}
func (b *Bmsteams) convertToMD(text string) string {
if !strings.Contains(text, "<div>") {
return text
}
var sb strings.Builder
err := godown.Convert(&sb, strings.NewReader(text), nil)
if err != nil {
b.Log.Errorf("Couldn't convert message to markdown %s", text)
return text
}
return sb.String()
}

View File

@@ -1,90 +0,0 @@
package bmumble
import (
"strconv"
"time"
"layeh.com/gumble/gumble"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
)
func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) {
b.serverConfigUpdate <- *event
}
func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
sender := "unknown"
if event.TextMessage.Sender != nil {
sender = event.TextMessage.Sender.Name
}
// Convert Mumble HTML messages to markdown
parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message)
if err != nil {
b.Log.Error(err)
}
now := time.Now().UTC()
for i, part := range parts {
// Construct matterbridge message and pass on to the gateway
rmsg := config.Message{
Channel: strconv.FormatUint(uint64(event.Client.Self.Channel.ID), 10),
Username: sender,
UserID: sender + "@" + b.Host,
Account: b.Account,
}
if part.Image == nil {
rmsg.Text = part.Text
} else {
fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + part.FileExtension
rmsg.Extra = make(map[string][]interface{})
if err = helper.HandleDownloadSize(b.Log, &rmsg, fname, int64(len(part.Image)), b.General); err != nil {
b.Log.WithError(err).Warn("not including image in message")
continue
}
helper.HandleDownloadData(b.Log, &rmsg, fname, "", "", &part.Image, b.General)
}
b.Log.Debugf("Sending message to gateway: %+v", rmsg)
b.Remote <- rmsg
}
}
func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
// Set the user's "bio"/comment
if comment := b.GetString("UserComment"); comment != "" && event.Client.Self != nil {
event.Client.Self.SetComment(comment)
}
// No need to talk or listen
event.Client.Self.SetSelfDeafened(true)
event.Client.Self.SetSelfMuted(true)
// if the Channel variable is set, this is a reconnect -> rejoin channel
if b.Channel != nil {
if err := b.doJoin(event.Client, *b.Channel); err != nil {
b.Log.Error(err)
}
b.Remote <- config.Message{
Username: "system",
Text: "rejoin",
Channel: "",
Account: b.Account,
Event: config.EventRejoinChannels,
}
}
}
func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) {
// Only care about changes to self
if event.User != event.Client.Self {
return
}
// Someone attempted to move the user out of the configured channel; attempt to join back
if b.Channel != nil {
if err := b.doJoin(event.Client, *b.Channel); err != nil {
b.Log.Error(err)
}
}
}
func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) {
b.connected <- *event
}

View File

@@ -1,143 +0,0 @@
package bmumble
import (
"fmt"
"mime"
"net/http"
"regexp"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/mattn/godown"
"github.com/vincent-petithory/dataurl"
)
type MessagePart struct {
Text string
FileExtension string
Image []byte
}
func (b *Bmumble) decodeImage(uri string, parts *[]MessagePart) error {
// Decode the data:image/... URI
image, err := dataurl.DecodeString(uri)
if err != nil {
b.Log.WithError(err).Info("No image extracted")
return err
}
// Determine the file extensions for that image
ext, err := mime.ExtensionsByType(image.MediaType.ContentType())
if err != nil || len(ext) == 0 {
b.Log.WithError(err).Infof("No file extension registered for MIME type '%s'", image.MediaType.ContentType())
return err
}
// Add the image to the MessagePart slice
*parts = append(*parts, MessagePart{"", ext[0], image.Data})
return nil
}
func (b *Bmumble) tokenize(t *string) ([]MessagePart, error) {
// `^(.*?)` matches everything before the image
// `!\[[^\]]*\]\(` matches the `![alt](` part of markdown images
// `(data:image\/[^)]+)` matches the data: URI used by Mumble
// `\)` matches the closing parenthesis after the URI
// `(.*)$` matches the remaining text to be examined in the next iteration
p := regexp.MustCompile(`^(?ms)(.*?)!\[[^\]]*\]\((data:image\/[^)]+)\)(.*)$`)
remaining := *t
var parts []MessagePart
for {
tokens := p.FindStringSubmatch(remaining)
if tokens == nil {
// no match -> remaining string is non-image text
pre := strings.TrimSpace(remaining)
if len(pre) > 0 {
parts = append(parts, MessagePart{pre, "", nil})
}
return parts, nil
}
// tokens[1] is the text before the image
if len(tokens[1]) > 0 {
pre := strings.TrimSpace(tokens[1])
parts = append(parts, MessagePart{pre, "", nil})
}
// tokens[2] is the image URL
uri, err := dataurl.UnescapeToString(strings.TrimSpace(strings.ReplaceAll(tokens[2], " ", "")))
if err != nil {
b.Log.WithError(err).Info("URL unescaping failed")
remaining = strings.TrimSpace(tokens[3])
continue
}
err = b.decodeImage(uri, &parts)
if err != nil {
b.Log.WithError(err).Info("Decoding the image failed")
}
// tokens[3] is the text after the image, processed in the next iteration
remaining = strings.TrimSpace(tokens[3])
}
}
func (b *Bmumble) convertHTMLtoMarkdown(html string) ([]MessagePart, error) {
var sb strings.Builder
err := godown.Convert(&sb, strings.NewReader(html), nil)
if err != nil {
return nil, err
}
markdown := sb.String()
b.Log.Debugf("### to markdown: %s", markdown)
return b.tokenize(&markdown)
}
func (b *Bmumble) extractFiles(msg *config.Message) []config.Message {
var messages []config.Message
if msg.Extra == nil || len(msg.Extra["file"]) == 0 {
return messages
}
// Create a separate message for each file
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
imsg := config.Message{
Channel: msg.Channel,
Username: msg.Username,
UserID: msg.UserID,
Account: msg.Account,
Protocol: msg.Protocol,
Timestamp: msg.Timestamp,
Event: "mumble_image",
}
// If no data is present for the file, send a link instead
if fi.Data == nil || len(*fi.Data) == 0 {
if len(fi.URL) > 0 {
imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL)
messages = append(messages, imsg)
} else {
b.Log.Infof("Not forwarding file without local data")
}
continue
}
mimeType := http.DetectContentType(*fi.Data)
// Mumble only supports images natively, send a link instead
if !strings.HasPrefix(mimeType, "image/") {
if len(fi.URL) > 0 {
imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL)
messages = append(messages, imsg)
} else {
b.Log.Infof("Not forwarding file of type %s", mimeType)
}
continue
}
mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0])
// Build data:image/...;base64,... style image URL and embed image directly into the message
du := dataurl.New(*fi.Data, mimeType)
dataURL, err := du.MarshalText()
if err != nil {
b.Log.WithError(err).Infof("Image Serialization into data URL failed (type: %s, length: %d)", mimeType, len(*fi.Data))
continue
}
imsg.Text = fmt.Sprintf(`<img src="%s"/>`, dataURL)
messages = append(messages, imsg)
}
// Remove files from original message
msg.Extra["file"] = nil
return messages
}

View File

@@ -1,259 +0,0 @@
package bmumble
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net"
"strconv"
"time"
"layeh.com/gumble/gumble"
"layeh.com/gumble/gumbleutil"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
stripmd "github.com/writeas/go-strip-markdown"
// 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 Bmumble struct {
client *gumble.Client
Nick string
Host string
Channel *uint32
local chan config.Message
running chan error
connected chan gumble.DisconnectEvent
serverConfigUpdate chan gumble.ServerConfigEvent
serverConfig gumble.ServerConfigEvent
tlsConfig tls.Config
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bmumble{}
b.Config = cfg
b.Nick = b.GetString("Nick")
b.local = make(chan config.Message)
b.running = make(chan error)
b.connected = make(chan gumble.DisconnectEvent)
b.serverConfigUpdate = make(chan gumble.ServerConfigEvent)
return b
}
func (b *Bmumble) Connect() error {
b.Log.Infof("Connecting %s", b.GetString("Server"))
host, portstr, err := net.SplitHostPort(b.GetString("Server"))
if err != nil {
return err
}
b.Host = host
_, err = strconv.Atoi(portstr)
if err != nil {
return err
}
if err = b.buildTLSConfig(); err != nil {
return err
}
go b.doSend()
go b.connectLoop()
err = <-b.running
return err
}
func (b *Bmumble) Disconnect() error {
return b.client.Disconnect()
}
func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
cid, err := strconv.ParseUint(channel.Name, 10, 32)
if err != nil {
return err
}
channelID := uint32(cid)
if b.Channel != nil && *b.Channel != channelID {
b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel)
return errors.New("the Mumble bridge can only join a single channel")
}
b.Channel = &channelID
return b.doJoin(b.client, channelID)
}
func (b *Bmumble) Send(msg config.Message) (string, error) {
// Only process text messages
b.Log.Debugf("=> Received local message %#v", msg)
if msg.Event != "" && msg.Event != config.EventUserAction {
return "", nil
}
attachments := b.extractFiles(&msg)
b.local <- msg
for _, a := range attachments {
b.local <- a
}
return "", nil
}
func (b *Bmumble) buildTLSConfig() error {
b.tlsConfig = tls.Config{}
// Load TLS client certificate keypair required for registered user authentication
if cpath := b.GetString("TLSClientCertificate"); cpath != "" {
if ckey := b.GetString("TLSClientKey"); ckey != "" {
cert, err := tls.LoadX509KeyPair(cpath, ckey)
if err != nil {
return err
}
b.tlsConfig.Certificates = []tls.Certificate{cert}
}
}
// Load TLS CA used for server verification. If not provided, the Go system trust anchor is used
if capath := b.GetString("TLSCACertificate"); capath != "" {
ca, err := ioutil.ReadFile(capath)
if err != nil {
return err
}
b.tlsConfig.RootCAs = x509.NewCertPool()
b.tlsConfig.RootCAs.AppendCertsFromPEM(ca)
}
b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify")
return nil
}
func (b *Bmumble) connectLoop() {
firstConnect := true
for {
err := b.doConnect()
if firstConnect {
b.running <- err
}
if err != nil {
b.Log.Errorf("Connection to server failed: %#v", err)
if firstConnect {
break
} else {
b.Log.Info("Retrying in 10s")
time.Sleep(10 * time.Second)
continue
}
}
firstConnect = false
d := <-b.connected
switch d.Type {
case gumble.DisconnectError:
b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String)
continue
case gumble.DisconnectKicked:
b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String)
continue
case gumble.DisconnectBanned:
b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String)
close(b.connected)
close(b.running)
return
case gumble.DisconnectUser:
b.Log.Infof("Disconnect successful")
close(b.connected)
close(b.running)
return
}
}
}
func (b *Bmumble) doConnect() error {
// Create new gumble config and attach event handlers
gumbleConfig := gumble.NewConfig()
gumbleConfig.Attach(gumbleutil.Listener{
ServerConfig: b.handleServerConfig,
TextMessage: b.handleTextMessage,
Connect: b.handleConnect,
Disconnect: b.handleDisconnect,
UserChange: b.handleUserChange,
})
gumbleConfig.Username = b.GetString("Nick")
if password := b.GetString("Password"); password != "" {
gumbleConfig.Password = password
}
client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
if err != nil {
return err
}
b.client = client
return nil
}
func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error {
channel, ok := client.Channels[channelID]
if !ok {
return fmt.Errorf("no channel with ID %d", channelID)
}
client.Self.Move(channel)
return nil
}
func (b *Bmumble) doSend() {
// Message sending loop that makes sure server-side
// restrictions and client-side message traits don't conflict
// with each other.
for {
select {
case serverConfig := <-b.serverConfigUpdate:
b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength)
b.serverConfig = serverConfig
case msg := <-b.local:
b.processMessage(&msg)
}
}
}
func (b *Bmumble) processMessage(msg *config.Message) {
b.Log.Debugf("Processing message %s", msg.Text)
allowHTML := true
if b.serverConfig.AllowHTML != nil {
allowHTML = *b.serverConfig.AllowHTML
}
// If this is a specially generated image message, send it unmodified
if msg.Event == "mumble_image" {
if allowHTML {
b.client.Self.Channel.Send(msg.Username+msg.Text, false)
} else {
b.Log.Info("Can't send image, server does not allow HTML messages")
}
return
}
// Don't process empty messages
if len(msg.Text) == 0 {
return
}
// If HTML is allowed, convert markdown into HTML, otherwise strip markdown
if allowHTML {
msg.Text = helper.ParseMarkdown(msg.Text)
} else {
msg.Text = stripmd.Strip(msg.Text)
}
// If there is a maximum message length, split and truncate the lines
var msgLines []string
if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username))
} else {
msgLines = helper.GetSubLines(msg.Text, 0)
}
// Send the individual lindes
for i := range msgLines {
b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
}
}

View File

@@ -1,203 +0,0 @@
package nctalk
import (
"context"
"crypto/tls"
"strconv"
"strings"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"gomod.garykim.dev/nc-talk/ocs"
"gomod.garykim.dev/nc-talk/room"
"gomod.garykim.dev/nc-talk/user"
)
type Btalk struct {
user *user.TalkUser
rooms []Broom
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Btalk{Config: cfg}
}
type Broom struct {
room *room.TalkRoom
ctx context.Context
ctxCancel context.CancelFunc
}
func (b *Btalk) Connect() error {
b.Log.Info("Connecting")
tconfig := &user.TalkUserConfig{
TLSConfig: &tls.Config{
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), //nolint:gosec
},
}
var err error
b.user, err = user.NewUser(b.GetString("Server"), b.GetString("Login"), b.GetString("Password"), tconfig)
if err != nil {
b.Log.Error("Config could not be used")
return err
}
_, err = b.user.Capabilities()
if err != nil {
b.Log.Error("Cannot Connect")
return err
}
b.Log.Info("Connected")
return nil
}
func (b *Btalk) Disconnect() error {
for _, r := range b.rooms {
r.ctxCancel()
}
return nil
}
func (b *Btalk) JoinChannel(channel config.ChannelInfo) error {
tr, err := room.NewTalkRoom(b.user, channel.Name)
if err != nil {
return err
}
newRoom := Broom{
room: tr,
}
newRoom.ctx, newRoom.ctxCancel = context.WithCancel(context.Background())
c, err := newRoom.room.ReceiveMessages(newRoom.ctx)
if err != nil {
return err
}
b.rooms = append(b.rooms, newRoom)
// Config
guestSuffix := " (Guest)"
if b.IsKeySet("GuestSuffix") {
guestSuffix = b.GetString("GuestSuffix")
}
go func() {
for msg := range c {
msg := msg
// ignore messages that are one of the following
// * not a message from a user
// * from ourselves
if msg.MessageType != ocs.MessageComment || msg.ActorID == b.user.User {
continue
}
remoteMessage := config.Message{
Text: formatRichObjectString(msg.Message, msg.MessageParameters),
Channel: newRoom.room.Token,
Username: DisplayName(msg, guestSuffix),
UserID: msg.ActorID,
Account: b.Account,
}
// It is possible for the ID to not be set on older versions of Talk so we only set it if
// the ID is not blank
if msg.ID != 0 {
remoteMessage.ID = strconv.Itoa(msg.ID)
}
// Handle Files
err = b.handleFiles(&remoteMessage, &msg)
if err != nil {
b.Log.Errorf("Error handling file: %#v", msg)
continue
}
b.Log.Debugf("<= Message is %#v", remoteMessage)
b.Remote <- remoteMessage
}
}()
return nil
}
func (b *Btalk) Send(msg config.Message) (string, error) {
r := b.getRoom(msg.Channel)
if r == nil {
b.Log.Errorf("Could not find room for %v", msg.Channel)
return "", nil
}
// Talk currently only supports sending normal messages
if msg.Event != "" {
return "", nil
}
sentMessage, err := r.room.SendMessage(msg.Username + msg.Text)
if err != nil {
b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err)
return "", nil
}
return strconv.Itoa(sentMessage.ID), nil
}
func (b *Btalk) getRoom(token string) *Broom {
for _, r := range b.rooms {
if r.room.Token == token {
return &r
}
}
return nil
}
func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error {
for _, parameter := range message.MessageParameters {
if parameter.Type == ocs.ROSTypeFile {
// Get the file
file, err := b.user.DownloadFile(parameter.Path)
if err != nil {
return err
}
if mmsg.Extra == nil {
mmsg.Extra = make(map[string][]interface{})
}
mmsg.Extra["file"] = append(mmsg.Extra["file"], config.FileInfo{
Name: parameter.Name,
Data: file,
Size: int64(len(*file)),
Avatar: false,
})
}
}
return nil
}
// Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785
func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string {
for id, parameter := range parameters {
text := parameter.Name
switch parameter.Type {
case ocs.ROSTypeUser, ocs.ROSTypeGroup:
text = "@" + text
case ocs.ROSTypeFile:
if parameter.Link != "" {
text = parameter.Name
}
}
message = strings.ReplaceAll(message, "{"+id+"}", text)
}
return message
}
func DisplayName(msg ocs.TalkRoomMessageData, suffix string) string {
if msg.ActorType == ocs.ActorGuest {
if msg.ActorDisplayName == "" {
return "Guest"
}
return msg.ActorDisplayName + suffix
}
return msg.ActorDisplayName
}

View File

@@ -2,7 +2,6 @@ package brocketchat
import ( import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
) )
func (b *Brocketchat) handleRocket() { func (b *Brocketchat) handleRocket() {
@@ -39,23 +38,6 @@ func (b *Brocketchat) handleRocketHook(messages chan *config.Message) {
} }
} }
func (b *Brocketchat) handleStatusEvent(ev models.Message, rmsg *config.Message) bool {
switch ev.Type {
case "":
// this is a normal message, no processing needed
// return true so the message is not dropped
return true
case sUserJoined, sUserLeft:
rmsg.Event = config.EventJoinLeave
return true
case sRoomChangedTopic:
rmsg.Event = config.EventTopicChange
return true
}
b.Log.Debugf("Dropping message with unknown type: %s", ev.Type)
return false
}
func (b *Brocketchat) handleRocketClient(messages chan *config.Message) { func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
for message := range b.messageChan { for message := range b.messageChan {
// skip messages with same ID, apparently messages get duplicated for an unknown reason // skip messages with same ID, apparently messages get duplicated for an unknown reason
@@ -77,14 +59,9 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
UserID: message.User.ID, UserID: message.User.ID,
ID: message.ID, ID: message.ID,
} }
// handleStatusEvent returns false if the message should be dropped
// in that case it is probably some modification to the channel we do not want to relay
if b.handleStatusEvent(m, rmsg) {
messages <- rmsg messages <- rmsg
} }
} }
}
func (b *Brocketchat) handleUploadFile(msg *config.Message) error { func (b *Brocketchat) handleUploadFile(msg *config.Message) error {
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {

View File

@@ -58,9 +58,6 @@ func (b *Brocketchat) doConnectWebhookURL() error {
func (b *Brocketchat) apiLogin() error { func (b *Brocketchat) apiLogin() error {
b.Log.Debugf("handling apiLogin()") b.Log.Debugf("handling apiLogin()")
credentials := &models.UserCredentials{Email: b.GetString("login"), Password: b.GetString("password")} credentials := &models.UserCredentials{Email: b.GetString("login"), Password: b.GetString("password")}
if b.GetString("Token") != "" {
credentials = &models.UserCredentials{ID: b.GetString("Login"), Token: b.GetString("Token")}
}
myURL, err := url.Parse(b.GetString("server")) myURL, err := url.Parse(b.GetString("server"))
if err != nil { if err != nil {
return err return err

View File

@@ -29,12 +29,6 @@ type Brocketchat struct {
sync.RWMutex sync.RWMutex
} }
const (
sUserJoined = "uj"
sUserLeft = "ul"
sRoomChangedTopic = "room_changed_topic"
)
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
newCache, err := lru.New(100) newCache, err := lru.New(100)
if err != nil { if err != nil {

View File

@@ -1,22 +1,18 @@
package bslack package bslack
import ( import (
"errors"
"fmt" "fmt"
"html" "html"
"time" "time"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/slack-go/slack" "github.com/nlopes/slack"
) )
// ErrEventIgnored is for events that should be ignored
var ErrEventIgnored = errors.New("this event message should ignored")
func (b *Bslack) handleSlack() { func (b *Bslack) handleSlack() {
messages := make(chan *config.Message) messages := make(chan *config.Message)
if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" { if b.GetString(incomingWebhookConfig) != "" {
b.Log.Debugf("Choosing webhooks based receiving") b.Log.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(messages) go b.handleMatterHook(messages)
} else { } else {
@@ -34,7 +30,6 @@ func (b *Bslack) handleSlack() {
message.Text = b.replaceVariable(message.Text) message.Text = b.replaceVariable(message.Text)
message.Text = b.replaceChannel(message.Text) message.Text = b.replaceChannel(message.Text)
message.Text = b.replaceURL(message.Text) message.Text = b.replaceURL(message.Text)
message.Text = b.replaceb0rkedMarkDown(message.Text)
message.Text = html.UnescapeString(message.Text) message.Text = html.UnescapeString(message.Text)
// Add the avatar // Add the avatar
@@ -48,7 +43,7 @@ func (b *Bslack) handleSlack() {
func (b *Bslack) handleSlackClient(messages chan *config.Message) { func (b *Bslack) handleSlackClient(messages chan *config.Message) {
for msg := range b.rtm.IncomingEvents { for msg := range b.rtm.IncomingEvents {
if msg.Type != sUserTyping && msg.Type != sHello && msg.Type != sLatencyReport { if msg.Type != sUserTyping && msg.Type != sLatencyReport {
b.Log.Debugf("== Receiving event %#v", msg.Data) b.Log.Debugf("== Receiving event %#v", msg.Data)
} }
switch ev := msg.Data.(type) { switch ev := msg.Data.(type) {
@@ -57,9 +52,7 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
continue continue
} }
rmsg, err := b.handleTypingEvent(ev) rmsg, err := b.handleTypingEvent(ev)
if err == ErrEventIgnored { if err != nil {
continue
} else if err != nil {
b.Log.Errorf("%#v", err) b.Log.Errorf("%#v", err)
continue continue
} }
@@ -93,7 +86,7 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
case *slack.MemberJoinedChannelEvent: case *slack.MemberJoinedChannelEvent:
b.users.populateUser(ev.User) b.users.populateUser(ev.User)
case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent: case *slack.LatencyReport:
continue continue
default: default:
b.Log.Debugf("Unhandled incoming event: %T", ev) b.Log.Debugf("Unhandled incoming event: %T", ev)
@@ -130,11 +123,11 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
} }
} }
// Check for our callback ID // Skip any messages that we made ourselves or from 'slackbot' (see #527).
hasOurCallbackID := false if ev.Username == sSlackBotUser ||
if len(ev.Blocks.BlockSet) == 1 { (b.rtm != nil && ev.Username == b.si.User.Name) ||
block, ok := ev.Blocks.BlockSet[0].(*slack.SectionBlock) (len(ev.Attachments) > 0 && ev.Attachments[0].CallbackID == "matterbridge_"+b.uuid) {
hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid return true
} }
if ev.SubMessage != nil { if ev.SubMessage != nil {
@@ -149,16 +142,6 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
if ev.SubType == "message_replied" && ev.Hidden { if ev.SubType == "message_replied" && ev.Hidden {
return true return true
} }
if len(ev.SubMessage.Blocks.BlockSet) == 1 {
block, ok := ev.SubMessage.Blocks.BlockSet[0].(*slack.SectionBlock)
hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid
}
}
// 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) || hasOurCallbackID {
return true
} }
if len(ev.Files) > 0 { if len(ev.Files) > 0 {
@@ -286,9 +269,6 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
} }
func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) { func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) {
if ev.User == b.si.User.ID {
return nil, ErrEventIgnored
}
channelInfo, err := b.channels.getChannelByID(ev.Channel) channelInfo, err := b.channels.getChannelByID(ev.Channel)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -7,8 +7,8 @@ import (
"time" "time"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/nlopes/slack"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/slack-go/slack"
) )
// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the // populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
@@ -188,36 +188,6 @@ func (b *Bslack) replaceURL(text string) string {
return text return text
} }
func (b *Bslack) replaceb0rkedMarkDown(text string) string {
// taken from https://github.com/mattermost/mattermost-server/blob/master/app/slackimport.go
//
regexReplaceAllString := []struct {
regex *regexp.Regexp
rpl string
}{
// bold
{
regexp.MustCompile(`(^|[\s.;,])\*(\S[^*\n]+)\*`),
"$1**$2**",
},
// strikethrough
{
regexp.MustCompile(`(^|[\s.;,])\~(\S[^~\n]+)\~`),
"$1~~$2~~",
},
// single paragraph blockquote
// Slack converts > character to &gt;
{
regexp.MustCompile(`(?sm)^&gt;`),
">",
},
}
for _, rule := range regexReplaceAllString {
text = rule.regex.ReplaceAllString(text, rule.rpl)
}
return text
}
func (b *Bslack) replaceCodeFence(text string) string { func (b *Bslack) replaceCodeFence(text string) string {
return codeFenceRE.ReplaceAllString(text, "```") return codeFenceRE.ReplaceAllString(text, "```")
} }

View File

@@ -5,7 +5,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
"github.com/slack-go/slack" "github.com/nlopes/slack"
) )
type BLegacy struct { type BLegacy struct {

View File

@@ -12,9 +12,9 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
lru "github.com/hashicorp/golang-lru" "github.com/hashicorp/golang-lru"
"github.com/nlopes/slack"
"github.com/rs/xid" "github.com/rs/xid"
"github.com/slack-go/slack"
) )
type Bslack struct { type Bslack struct {
@@ -36,7 +36,6 @@ type Bslack struct {
} }
const ( const (
sHello = "hello"
sChannelJoin = "channel_join" sChannelJoin = "channel_join"
sChannelLeave = "channel_leave" sChannelLeave = "channel_leave"
sChannelJoined = "channel_joined" sChannelJoined = "channel_joined"
@@ -64,7 +63,6 @@ const (
editSuffixConfig = "EditSuffix" editSuffixConfig = "EditSuffix"
iconURLConfig = "iconurl" iconURLConfig = "iconurl"
noSendJoinConfig = "nosendjoinpart" noSendJoinConfig = "nosendjoinpart"
messageLength = 3000
) )
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
@@ -195,7 +193,6 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg) b.Log.Debugf("=> Receiving %#v", msg)
} }
msg.Text = helper.ClipMessage(msg.Text, messageLength)
msg.Text = b.replaceCodeFence(msg.Text) msg.Text = b.replaceCodeFence(msg.Text)
// Make a action /me of the message // Make a action /me of the message
@@ -204,7 +201,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
} }
// Use webhook to send the message // Use webhook to send the message
if b.GetString(outgoingWebhookConfig) != "" && b.GetString(tokenConfig) == "" { if b.GetString(outgoingWebhookConfig) != "" {
return "", b.sendWebhook(msg) return "", b.sendWebhook(msg)
} }
return b.sendRTM(msg) return b.sendRTM(msg)
@@ -410,6 +407,7 @@ func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (b
} }
messageOptions := b.prepareMessageOptions(msg) messageOptions := b.prepareMessageOptions(msg)
for { for {
messageOptions = append(messageOptions, slack.MsgOptionText(msg.Text, false))
_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...) _, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...)
if err == nil { if err == nil {
return true, nil return true, nil
@@ -428,6 +426,11 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
return "", nil return "", nil
} }
messageOptions := b.prepareMessageOptions(msg) messageOptions := b.prepareMessageOptions(msg)
messageOptions = append(
messageOptions,
slack.MsgOptionText(msg.Text, false),
slack.MsgOptionEnableLinkUnfurl(),
)
for { for {
_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...) _, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
if err == nil { if err == nil {
@@ -493,6 +496,8 @@ func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
} }
var attachments []slack.Attachment 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 // add file attachments
attachments = append(attachments, b.createAttach(msg.Extra)...) attachments = append(attachments, b.createAttach(msg.Extra)...)
// add slack attachments (from another slack bridge) // add slack attachments (from another slack bridge)
@@ -503,19 +508,6 @@ func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
} }
var opts []slack.MsgOption var opts []slack.MsgOption
opts = append(opts,
// provide regular text field (fallback used in Slack notifications, etc.)
slack.MsgOptionText(msg.Text, false),
// add a callback ID so we can see we created it
slack.MsgOptionBlocks(slack.NewSectionBlock(
slack.NewTextBlockObject(slack.MarkdownType, msg.Text, false, false),
nil, nil,
slack.SectionBlockOptionBlockID("matterbridge_"+b.uuid),
)),
slack.MsgOptionEnableLinkUnfurl(),
)
opts = append(opts, slack.MsgOptionAttachments(attachments...)) opts = append(opts, slack.MsgOptionAttachments(attachments...))
opts = append(opts, slack.MsgOptionPostMessageParameters(params)) opts = append(opts, slack.MsgOptionPostMessageParameters(params))
return opts return opts

View File

@@ -8,8 +8,8 @@ import (
"time" "time"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/nlopes/slack"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/slack-go/slack"
) )
const minimumRefreshInterval = 10 * time.Second const minimumRefreshInterval = 10 * time.Second

View File

@@ -130,10 +130,6 @@ func (b *Bsshchat) handleSSHChat() error {
if strings.Contains(b.r.Text(), "Rate limiting is in effect") { if strings.Contains(b.r.Text(), "Rate limiting is in effect") {
continue continue
} }
// skip our own messages
if !strings.HasPrefix(b.r.Text(), "["+b.GetString("Nick")+"] \x1b") {
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

View File

@@ -85,7 +85,7 @@ func (b *Bsteam) handleEvents() {
func (b *Bsteam) handleLogOnFailed(e *steam.LogOnFailedEvent, myLoginInfo *steam.LogOnDetails) error { func (b *Bsteam) handleLogOnFailed(e *steam.LogOnFailedEvent, myLoginInfo *steam.LogOnDetails) error {
switch e.Result { switch e.Result {
case steamlang.EResult_AccountLoginDeniedNeedTwoFactor: case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode:
b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:") b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:")
var code string var code string
fmt.Scanf("%s", &code) fmt.Scanf("%s", &code)

View File

@@ -39,33 +39,23 @@ func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message
// handleForwarded handles forwarded messages // handleForwarded handles forwarded messages
func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) { func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) {
if message.ForwardDate == 0 { if message.ForwardFrom != nil {
return
}
if message.ForwardFrom == nil {
rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text
return
}
usernameForward := "" usernameForward := ""
if b.GetBool("UseFirstName") { if b.GetBool("UseFirstName") {
usernameForward = message.ForwardFrom.FirstName usernameForward = message.ForwardFrom.FirstName
} }
if usernameForward == "" { if usernameForward == "" {
usernameForward = message.ForwardFrom.UserName usernameForward = message.ForwardFrom.UserName
if usernameForward == "" { if usernameForward == "" {
usernameForward = message.ForwardFrom.FirstName usernameForward = message.ForwardFrom.FirstName
} }
} }
if usernameForward == "" { if usernameForward == "" {
usernameForward = unknownUser usernameForward = unknownUser
} }
rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
} }
}
// handleQuoting handles quoting of previous messages // handleQuoting handles quoting of previous messages
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) { func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
@@ -105,7 +95,7 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
} }
} }
// only download avatars if we have a place to upload them (configured mediaserver) // only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") { if b.General.MediaServerUpload != "" {
b.handleDownloadAvatar(message.From.ID, rmsg.Channel) b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
} }
} }
@@ -217,46 +207,6 @@ func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
} }
} }
func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) {
var format string
switch b.GetString("MediaConvertTgs") {
case FormatWebp:
b.Log.Debugf("Tgs to WebP conversion enabled, converting %v", name)
format = FormatWebp
case FormatPng:
// The WebP to PNG converter can't handle animated webp files yet,
// and I'm not going to write a path for x/image/webp.
// The error message would be:
// conversion failed: webp: non-Alpha VP8X is not implemented
// So instead, we tell lottie to directly go to PNG.
b.Log.Debugf("Tgs to PNG conversion enabled, converting %v", name)
format = FormatPng
default:
// Otherwise, no conversion was requested. Trying to run the usual webp
// converter would fail, because '.tgs.webp' is actually a gzipped JSON
// file, and has nothing to do with WebP.
return
}
err := helper.ConvertTgsToX(data, format, b.Log)
if err != nil {
b.Log.Errorf("conversion failed: %v", err)
} else {
*name = strings.Replace(*name, "tgs.webp", format, 1)
}
}
func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) {
if b.GetBool("MediaConvertWebPToPNG") {
b.Log.Debugf("WebP to PNG conversion enabled, converting %v", name)
err := helper.ConvertWebPToPNG(data)
if err != nil {
b.Log.Errorf("conversion failed: %v", err)
} else {
*name = strings.Replace(*name, ".webp", ".png", 1)
}
}
}
// handleDownloadFile handles file download // handleDownloadFile handles file download
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error { func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
size := 0 size := 0
@@ -304,13 +254,15 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
if err != nil { if err != nil {
return err return err
} }
if strings.HasSuffix(name, ".webp") && b.GetBool("MediaConvertWebPToPNG") {
if strings.HasSuffix(name, ".tgs.webp") { b.Log.Debugf("WebP to PNG conversion enabled, converting %s", name)
b.maybeConvertTgs(&name, data) err := helper.ConvertWebPToPNG(data)
} else if strings.HasSuffix(name, ".webp") { if err != nil {
b.maybeConvertWebp(&name, data) b.Log.Errorf("conversion failed: %s", err)
} else {
name = strings.Replace(name, ".webp", ".png", 1)
}
} }
helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General) helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
return nil return nil
} }
@@ -360,9 +312,6 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error
case "Markdown": case "Markdown":
b.Log.Debug("Using mode markdown") b.Log.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown m.ParseMode = tgbotapi.ModeMarkdown
case MarkdownV2:
b.Log.Debug("Using mode MarkdownV2")
m.ParseMode = MarkdownV2
} }
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only") b.Log.Debug("Using mode HTML - nick only")
@@ -408,14 +357,6 @@ func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string
if format == "" { if format == "" {
format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
} }
quoteMessagelength := len(quoteMessage)
if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") {
runes := []rune(quoteMessage)
quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")])
if quoteMessagelength > b.GetInt("QuoteLengthLimit") {
quoteMessage += "..."
}
}
format = strings.Replace(format, "{MESSAGE}", message, -1) format = strings.Replace(format, "{MESSAGE}", message, -1)
format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1) format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1)
format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1) format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)

View File

@@ -2,23 +2,19 @@ package btelegram
import ( import (
"html" "html"
"log"
"strconv" "strconv"
"strings" "strings"
"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/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" "github.com/go-telegram-bot-api/telegram-bot-api"
) )
const ( const (
unknownUser = "unknown" unknownUser = "unknown"
HTMLFormat = "HTML" HTMLFormat = "HTML"
HTMLNick = "htmlnick" HTMLNick = "htmlnick"
MarkdownV2 = "MarkdownV2"
FormatPng = "png"
FormatWebp = "webp"
) )
type Btelegram struct { type Btelegram struct {
@@ -28,16 +24,6 @@ type Btelegram struct {
} }
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
tgsConvertFormat := cfg.GetString("MediaConvertTgs")
if tgsConvertFormat != "" {
err := helper.CanConvertTgsToX()
if err != nil {
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but lottie does not appear to work:\n%#v", tgsConvertFormat, err)
}
if tgsConvertFormat != FormatPng && tgsConvertFormat != FormatWebp {
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but only '%s' and '%s' are supported.", FormatPng, FormatWebp, tgsConvertFormat)
}
}
return &Btelegram{Config: cfg, avatarMap: make(map[string]string)} return &Btelegram{Config: cfg, avatarMap: make(map[string]string)}
} }
@@ -95,8 +81,8 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
// Upload a file if it exists // Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { for _, rmsg := range helper.HandleExtra(&msg, b.General) {
if _, msgErr := b.sendMessage(chatid, rmsg.Username, rmsg.Text); msgErr != nil { if _, err := b.sendMessage(chatid, rmsg.Username, rmsg.Text); err != nil {
b.Log.Errorf("sendMessage failed: %s", msgErr) b.Log.Errorf("sendMessage 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)
@@ -111,16 +97,9 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
} }
// Post normal message // Post normal message
// TODO: recheck it.
// Ignore empty text field needs for prevent double messages from whatsapp to telegram
// when sending media with text caption
if msg.Text != "" {
return b.sendMessage(chatid, msg.Username, msg.Text) return b.sendMessage(chatid, msg.Username, msg.Text)
} }
return "", nil
}
func (b *Btelegram) getFileDirectURL(id string) string { func (b *Btelegram) getFileDirectURL(id string) string {
res, err := b.c.GetFileDirectURL(id) res, err := b.c.GetFileDirectURL(id)
if err != nil { if err != nil {
@@ -140,18 +119,11 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, er
b.Log.Debug("Using mode markdown") b.Log.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown m.ParseMode = tgbotapi.ModeMarkdown
} }
if b.GetString("MessageFormat") == MarkdownV2 {
b.Log.Debug("Using mode MarkdownV2")
m.ParseMode = MarkdownV2
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only") b.Log.Debug("Using mode HTML - nick only")
m.Text = username + html.EscapeString(text) m.Text = username + html.EscapeString(text)
m.ParseMode = tgbotapi.ModeHTML m.ParseMode = tgbotapi.ModeHTML
} }
m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
res, err := b.c.Send(m) res, err := b.c.Send(m)
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -1,15 +1,11 @@
package bwhatsapp package bwhatsapp
import ( import (
"fmt"
"mime"
"strings" "strings"
"time" "time"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
"github.com/jpillora/backoff"
) )
/* /*
@@ -23,43 +19,10 @@ Check:
// HandleError received from WhatsApp // HandleError received from WhatsApp
func (b *Bwhatsapp) HandleError(err error) { func (b *Bwhatsapp) HandleError(err error) {
// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843 // ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
// ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094 if strings.Contains(err.Error(), "error processing data: received invalid data") {
if strings.Contains(err.Error(), "error processing data: received invalid data") || strings.Contains(err.Error(), "invalid string with tag 174") {
return return
} }
b.Log.Errorf("%v", err) // TODO implement proper handling? at least respond to different error types
switch err.(type) {
case *whatsapp.ErrConnectionClosed, *whatsapp.ErrConnectionFailed:
b.reconnect(err)
default:
switch err {
case whatsapp.ErrConnectionTimeout:
b.reconnect(err)
default:
b.Log.Errorf("%v", err)
}
}
}
func (b *Bwhatsapp) reconnect(err error) {
bf := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
for {
d := bf.Duration()
b.Log.Errorf("Connection failed, underlying error: %v", err)
b.Log.Infof("Waiting %s...", d)
time.Sleep(d)
b.Log.Info("Reconnecting...")
err := b.conn.Restore()
if err == nil {
bf.Reset()
b.startedAt = uint64(time.Now().Unix())
return
}
}
} }
// HandleTextMessage sent from WhatsApp, relay it to the brige // HandleTextMessage sent from WhatsApp, relay it to the brige
@@ -73,18 +36,16 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
} }
messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
groupJID := message.Info.RemoteJid groupJid := message.Info.RemoteJid
senderJID := message.Info.SenderJid senderJid := message.Info.SenderJid
if len(senderJID) == 0 { if len(senderJid) == 0 {
// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved // TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
if message.Info.Source != nil && message.Info.Source.Participant != nil { senderJid = *message.Info.Source.Participant
senderJID = *message.Info.Source.Participant
}
} }
// translate sender's JID to the nicest username we can get // translate sender's Jid to the nicest username we can get
senderName := b.getSenderName(senderJID) senderName := b.getSenderName(senderJid)
if senderName == "" { if senderName == "" {
senderName = "Someone" // don't expose telephone number senderName = "Someone" // don't expose telephone number
} }
@@ -92,8 +53,8 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
extText := message.Info.Source.Message.ExtendedTextMessage extText := message.Info.Source.Message.ExtendedTextMessage
if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil { if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil {
// handle user mentions // handle user mentions
for _, mentionedJID := range extText.ContextInfo.MentionedJid { for _, mentionedJid := range extText.ContextInfo.MentionedJid {
numberAndSuffix := strings.SplitN(mentionedJID, "@", 2) numberAndSuffix := strings.SplitN(mentionedJid, "@", 2)
// mentions comes as telephone numbers and we don't want to expose it to other bridges // mentions comes as telephone numbers and we don't want to expose it to other bridges
// replace it with something more meaninful to others // replace it with something more meaninful to others
@@ -105,13 +66,13 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
} }
} }
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJid, b.Account)
rmsg := config.Message{ rmsg := config.Message{
UserID: senderJID, UserID: senderJid,
Username: senderName, Username: senderName,
Text: message.Text, Text: message.Text,
Timestamp: messageTime, Timestamp: messageTime,
Channel: groupJID, Channel: groupJid,
Account: b.Account, Account: b.Account,
Protocol: b.Protocol, Protocol: b.Protocol,
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
@@ -120,7 +81,7 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
// Gateway string // will be added during message processing // Gateway string // will be added during message processing
ID: message.Info.Id} ID: message.Info.Id}
if avatarURL, exists := b.userAvatars[senderJID]; exists { if avatarURL, exists := b.userAvatars[senderJid]; exists {
rmsg.Avatar = avatarURL rmsg.Avatar = avatarURL
} }
@@ -128,77 +89,11 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
b.Remote <- rmsg b.Remote <- rmsg
} }
// HandleImageMessage sent from WhatsApp, relay it to the brige //
func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) { //func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") { // fmt.Println(message) // TODO implement
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
if message.Info.Source != nil && message.Info.Source.Participant != nil {
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
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
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
}
// Download and unencrypt content
data, err := message.Download()
if err != nil {
b.Log.Errorf("%v", err)
return
}
// Get file extension by mimetype
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("%v", err)
return
}
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("<= Image downloaded and unencrypted")
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
b.Log.Debugf("<= Image Message is %#v", rmsg)
b.Remote <- rmsg
}
//func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) { //func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
// fmt.Println(message) // TODO implement // fmt.Println(message) // TODO implement
//} //}

View File

@@ -80,33 +80,8 @@ func (b *Bwhatsapp) getSenderName(senderJid string) string {
// if user is not in phone contacts // if user is not in phone contacts
// it is the most obvious scenario unless you sync your phone contacts with some remote updated source // 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 // users can change it in their WhatsApp settings -> profile -> click on Avatar
if sender.Notify != "" {
return sender.Notify return sender.Notify
} }
if sender.Short != "" {
return sender.Short
}
}
// try to reload this contact
_, err := b.conn.Contacts()
if err != nil {
b.Log.Errorf("error on update of contacts: %v", err)
}
if contact, exists := b.conn.Store.Contacts[senderJid]; exists {
// Add it to the user map
b.users[senderJid] = contact
if contact.Name != "" {
return contact.Name
}
// if user is not in phone contacts
// same as above
return contact.Notify
}
return "" return ""
} }

View File

@@ -1,14 +1,11 @@
package bwhatsapp package bwhatsapp
import ( import (
"bytes"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"mime"
"os" "os"
"path/filepath"
"strings" "strings"
"time" "time"
@@ -183,7 +180,7 @@ func (b *Bwhatsapp) Disconnect() error {
} }
func isGroupJid(identifier string) bool { func isGroupJid(identifier string) bool {
return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp") || strings.HasSuffix(identifier, "@broadcast") 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' // JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
@@ -233,66 +230,6 @@ func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
return nil return nil
} }
// Post a document message from the bridge to WhatsApp
func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
fi := msg.Extra["file"][0].(config.FileInfo)
// Post document message
message := whatsapp.DocumentMessage{
Info: whatsapp.MessageInfo{
RemoteJid: msg.Channel,
},
Title: fi.Name,
FileName: fi.Name,
Type: filetype,
Content: bytes.NewReader(*fi.Data),
}
b.Log.Debugf("=> Sending %#v", msg)
// create message ID
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
idBytes := make([]byte, 10)
if _, err := rand.Read(idBytes); err != nil {
b.Log.Warn(err.Error())
}
message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
_, err := b.conn.Send(message)
return message.Info.Id, err
}
// Post an image message from the bridge to WhatsApp
// Handle, for sure image/jpeg, image/png and image/gif MIME types
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
fi := msg.Extra["file"][0].(config.FileInfo)
// Post image message
message := whatsapp.ImageMessage{
Info: whatsapp.MessageInfo{
RemoteJid: msg.Channel,
},
Type: filetype,
Caption: msg.Username + fi.Comment,
Content: bytes.NewReader(*fi.Data),
}
b.Log.Debugf("=> Sending %#v", msg)
// create message ID
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
idBytes := make([]byte, 10)
if _, err := rand.Read(idBytes); err != nil {
b.Log.Warn(err.Error())
}
message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
_, err := b.conn.Send(message)
return message.Info.Id, err
}
// Send a message from the bridge to WhatsApp // Send a message from the bridge to WhatsApp
// Required implementation of the Bridger interface // Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16 // https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
@@ -322,25 +259,18 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
// TODO handle edit as a message reply with updated text // TODO handle edit as a message reply with updated text
} }
// Handle Upload a file //// TODO Handle Upload a file
if msg.Extra["file"] != nil { //if msg.Extra != nil {
fi := msg.Extra["file"][0].(config.FileInfo) // for _, rmsg := range helper.HandleExtra(&msg, b.General) {
filetype := mime.TypeByExtension(filepath.Ext(fi.Name)) // b.c.SendMessage(roomID, rmsg.Username+rmsg.Text)
// }
b.Log.Debugf("Extra file is %#v", filetype) // if len(msg.Extra["file"]) > 0 {
// return b.handleUploadFile(&msg, roomID)
// TODO: add different types // }
// TODO: add webp conversion //}
switch filetype {
case "image/jpeg", "image/png", "image/gif":
return b.PostImageMessage(msg, filetype)
default:
return b.PostDocumentMessage(msg, filetype)
}
}
// Post text message // Post text message
message := whatsapp.TextMessage{ text := whatsapp.TextMessage{
Info: whatsapp.MessageInfo{ Info: whatsapp.MessageInfo{
RemoteJid: msg.Channel, // which equals to group id RemoteJid: msg.Channel, // which equals to group id
}, },
@@ -351,14 +281,15 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
// create message ID // create message ID
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented // TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
idBytes := make([]byte, 10) bytes := make([]byte, 10)
if _, err := rand.Read(idBytes); err != nil { if _, err := rand.Read(bytes); err != nil {
b.Log.Warn(err.Error()) b.Log.Warn(err.Error())
} }
message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes)) text.Info.Id = strings.ToUpper(hex.EncodeToString(bytes))
_, err := b.conn.Send(message)
return message.Info.Id, err _, 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 // 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

View File

@@ -1,34 +0,0 @@
package bxmpp
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/go-xmpp"
)
// 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 *Bxmpp) handleDownloadAvatar(avatar xmpp.AvatarData) {
rmsg := config.Message{
Username: "system",
Text: "avatar",
Channel: b.parseChannel(avatar.From),
Account: b.Account,
UserID: avatar.From,
Event: config.EventAvatarDownload,
Extra: make(map[string][]interface{}),
}
if _, ok := b.avatarMap[avatar.From]; !ok {
b.Log.Debugf("Avatar.From: %s", avatar.From)
err := helper.HandleDownloadSize(b.Log, &rmsg, avatar.From+".png", int64(len(avatar.Data)), b.General)
if err != nil {
b.Log.Error(err)
return
}
helper.HandleDownloadData(b.Log, &rmsg, avatar.From+".png", rmsg.Text, "", &avatar.Data, b.General)
b.Log.Debugf("Avatar download complete")
b.Remote <- rmsg
}
}

View File

@@ -1,30 +0,0 @@
package bxmpp
import (
"regexp"
"github.com/42wim/matterbridge/bridge/config"
)
var pathRegex = regexp.MustCompile("[^a-zA-Z0-9]+")
// 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 hash, ok := av[userid]; ok {
// NOTE: This does not happen in bridge/helper/helper.go but messes up XMPP
id := pathRegex.ReplaceAllString(userid, "_")
return general.MediaServerDownload + "/" + hash + "/" + id + ".png"
}
return ""
}
func (b *Bxmpp) cacheAvatar(msg *config.Message) string {
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 ""
}

View File

@@ -23,17 +23,12 @@ type Bxmpp struct {
xmppMap map[string]string xmppMap map[string]string
connected bool connected bool
sync.RWMutex sync.RWMutex
avatarAvailability map[string]bool
avatarMap map[string]string
} }
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
return &Bxmpp{ return &Bxmpp{
Config: cfg, Config: cfg,
xmppMap: make(map[string]string), xmppMap: make(map[string]string),
avatarAvailability: make(map[string]bool),
avatarMap: make(map[string]string),
} }
} }
@@ -72,19 +67,8 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
if msg.Event == config.EventMsgDelete { if msg.Event == config.EventMsgDelete {
return "", nil return "", nil
} }
b.Log.Debugf("=> Receiving %#v", msg) b.Log.Debugf("=> Receiving %#v", msg)
if msg.Event == config.EventAvatarDownload {
return b.cacheAvatar(&msg), nil
}
// Make a action /me of the message, prepend the username with it.
// https://xmpp.org/extensions/xep-0245.html
if msg.Event == config.EventUserAction {
msg.Username = "/me " + msg.Username
}
// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support). // Upload a file (in XMPP case send the upload URL because XMPP has no native upload support).
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { for _, rmsg := range helper.HandleExtra(&msg, b.General) {
@@ -123,29 +107,24 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
} }
func (b *Bxmpp) createXMPP() error { func (b *Bxmpp) createXMPP() error {
if !strings.Contains(b.GetString("Jid"), "@") {
return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid"))
}
tc := &tls.Config{ tc := &tls.Config{
ServerName: strings.Split(b.GetString("Jid"), "@")[1], ServerName: strings.Split(b.GetString("Jid"), "@")[1],
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec
} }
xmpp.DebugWriter = b.Log.Writer()
options := xmpp.Options{ options := xmpp.Options{
Host: b.GetString("Server"), Host: b.GetString("Server"),
User: b.GetString("Jid"), User: b.GetString("Jid"),
Password: b.GetString("Password"), Password: b.GetString("Password"),
NoTLS: true, NoTLS: true,
StartTLS: !b.GetBool("NoTLS"), StartTLS: true,
TLSConfig: tc, TLSConfig: tc,
Debug: b.GetBool("debug"), Debug: b.GetBool("debug"),
Logger: b.Log.Writer(),
Session: true, Session: true,
Status: "", Status: "",
StatusMessage: "", StatusMessage: "",
Resource: "", Resource: "",
InsecureAllowUnencryptedAuth: b.GetBool("NoTLS"), InsecureAllowUnencryptedAuth: false,
} }
var err error var err error
b.xc, err = options.NewClient() b.xc, err = options.NewClient()
@@ -246,16 +225,6 @@ func (b *Bxmpp) handleXMPP() error {
event = config.EventTopicChange event = config.EventTopicChange
} }
available, sok := b.avatarAvailability[v.Remote]
avatar := ""
if !sok {
b.Log.Debugf("Requesting avatar data")
b.avatarAvailability[v.Remote] = false
b.xc.AvatarRequestData(v.Remote)
} else if available {
avatar = getAvatar(b.avatarMap, v.Remote, b.General)
}
msgID := v.ID msgID := v.ID
if v.ReplaceID != "" { if v.ReplaceID != "" {
msgID = v.ReplaceID msgID = v.ReplaceID
@@ -265,7 +234,6 @@ func (b *Bxmpp) handleXMPP() error {
Text: v.Text, Text: v.Text,
Channel: b.parseChannel(v.Remote), Channel: b.parseChannel(v.Remote),
Account: b.Account, Account: b.Account,
Avatar: avatar,
UserID: v.Remote, UserID: v.Remote,
ID: msgID, ID: msgID,
Event: event, Event: event,
@@ -282,10 +250,6 @@ func (b *Bxmpp) handleXMPP() error {
b.Log.Debugf("<= Message is %#v", rmsg) b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
} }
case xmpp.AvatarData:
b.handleDownloadAvatar(v)
b.avatarAvailability[v.From] = true
b.Log.Debugf("Avatar for %s is now available", v.From)
case xmpp.Presence: case xmpp.Presence:
// Do nothing. // Do nothing.
} }

View File

@@ -135,25 +135,19 @@ func (b *Bzulip) handleQueue() error {
if m.SenderEmail == b.GetString("login") { if m.SenderEmail == b.GetString("login") {
continue continue
} }
avatarURL := m.AvatarURL
if !strings.HasPrefix(avatarURL, "http") {
avatarURL = b.GetString("server") + avatarURL
}
rmsg := config.Message{ rmsg := config.Message{
Username: m.SenderFullName, Username: m.SenderFullName,
Text: m.Content, Text: m.Content,
Channel: b.getChannel(m.StreamID) + "/topic:" + m.Subject, Channel: b.getChannel(m.StreamID) + "/topic:" + m.Subject,
Account: b.Account, Account: b.Account,
UserID: strconv.Itoa(m.SenderID), UserID: strconv.Itoa(m.SenderID),
Avatar: avatarURL, Avatar: m.AvatarURL,
} }
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg) b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
b.q.LastEventID = m.ID
} }
time.Sleep(time.Second * 3) time.Sleep(time.Second * 3)
} }
} }

View File

@@ -1,357 +1,12 @@
# v1.20.0 # dev
## Breaking
- matrix: Send the display name instead of the user name (matrix) (#1282)
Matrix now sends the displayname if set instead of the username. If you want to keep the username, add `UseUsername=true` to your matrix config. <https://github.com/42wim/matterbridge/wiki/Settings#useusername-1>
- discord: Disable webhook editing (discord) (#1296)
Because of issues with ratelimiting of webhook editing, this feature is now disabled. If you have multiple discord channels you bridge, you'll need to add a `webhookURL` to the `[gateway.inout.options]`. See <https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample#L1864-L1870> for an example.
## New features
- general: Allow tengo to drop messages using msgDrop (#1272)
- general: Update libraries (whatsapp,markdown,mattermost,ssh-chat)
- irc: Add PingDelay option (irc) (#1269)
- matrix: Allow message edits on matrix (#1286)
- xmpp: add NoTLS option to allow plaintext XMPP connections (#1288)
## Enhancements
- discord: Edit messages via webhook (1287)
- general: Add extra debug to log time spent sending a message per bridge (#1299)
This release couldn't exist without the following contributors:
@nightmared, @zhoreeq
# v1.19.0
## New features
- mumble: new protocol added: Add Mumble support (#1245)
- nctalk: Add support for downloading files (nctalk) (#1249)
- nctalk: Append a suffix if user is a guest user (nctalk) (#1250)
## Enhancements
- irc: Add even more debug for irc (#1266)
- matrix: Add username formatting for all events (matrix) (#1233)
- matrix: Permit uploading files of other mimetypes (#1237)
- whatsapp: Use vendored whatsapp version (#1258)
- whatsapp: Add username for images from WhatsApp (#1232)
This release couldn't exist without the following contributors:
@Dellle, @42wim, @gary-kim, @s3lph, @BenWiederhake
# v1.18.3
## Enhancements
- nctalk: Add TLSConfig to nctalk (#1195)
- whatsapp: Handle broadcasts as groups in Whatsapp #1213
- matrix: switch to upstream gomatrix #1219
- api: support multiple websocket clients #1205
## Bugfix
- general: update vendor
- zulip: Check location of avatarURL (zulip). Fixes #1214 (#1227)
- nctalk: Fix issue with too many open files #1223
- nctalk: Fix mentions #1222
- nctalk: Fix message replays #1220
This release couldn't exist without the following contributors:
@gary-kim, @tilosp, @NikkyAI, @escoand, @42wim
# v1.18.2
## Bugfix
- zulip: Fix error loop (zulip) (#1210)
- whatsapp: Update whatsapp vendor and fix a panic (#1209)
This release couldn't exist without the following contributors:
@SuperSandro2000, @42wim
# v1.18.1
## New features
- telegram: Support Telegram animated stickers (tgs) format (#1173). See https://github.com/42wim/matterbridge/wiki/Settings#mediaConverttgs for more info
## Enhancements
- matrix: Remove HTML formatting for push messages (#1188) (#1189)
- mattermost: Use mattermost v5 module (#1192)
## Bugfix
- whatsapp: Handle panic in whatsapp. Fixes #1180 (#1184)
- nctalk: Fix Nextcloud Talk connection failure (#1179)
- matrix: Sleep when ratelimited on joins (matrix). Fixes #1201 (#1206)
This release couldn't exist without the following contributors:
@42wim, @BenWiederhake, @Dellle, @gary-kim
# v1.18.0
## New features
- nctalk: new protocol added. Add Nextcloud Talk support #1167
- general: Add an option to log into a file rather than stdout (#1168)
- api: Add websocket to API (#970)
## Enhancements
- telegram: Fix MarkdownV2 support in Telegram (#1169)
- whatsapp: Reload user information when a new contact is detected (whatsapp) (#1160)
- api: Add sane RemoteNickFormat default for API (#1157)
- irc: Skip gIRC built-in rate limiting (irc) (#1164)
- irc: Only colour IRC nicks if there is one. (#1161)
- docker: Combine runs to one layer (#1151)
## Bugfix
- general: Update dependencies for 1.18.0 release (#1175)
Discord users are encouraged to upgrade, this release works with the move to the discord.com domain.
This release couldn't exist without the following contributors:
@42wim, @jlu5, @qaisjp, @TheHolyRoger, @SuperSandro2000, @gary-kim, @z3bra, @greenx, @haykam821, @nathanaelhoun
# v1.17.5
## Enhancements
- irc: Add StripMarkdown option (irc). (#1145)
- general: Increase debug logging with function,file and linenumber (#1147)
- general: Update Dockerfile so inotify works (#1148)
- matrix: Add an option to disable sending HTML to matrix. Fixes #1022 (#1135)
- xmpp: Implement xep-0245 (xmpp). Closes #1137 (#1144)
## Bugfix
- discord: Fix #1120: replaceAction "_" crash (discord) (#1121)
- discord: Fix #1049: missing space before embeds (discord) (#1124)
- discord: Fix webhook EventUserAction messages being skipped (discord) (#1133)
- matrix: Avoid creating invalid url when the user doesn't have an avatar (matrix) (#1130)
- msteams: Ignore non-user messages (msteams). Fixes #1141 (#1149)
- slack: Do not use webhooks when token is configured (slack) (fixes #1123) (#1134)
- telegram: Fix forward from hidden users (telegram). Closes #1131 (#1143)
- xmpp: Prevent re-requesting avatar data (xmpp) (#1117)
This release couldn't exist without the following contributors:
@qaisjp, @xnaas, @42wim, @Polynomdivision, @tfve
# v1.17.4
## Bugfix
- general: Lowercase account names. Fixes #1108 (#1110)
- msteams: Remove panics and retry polling on failure (msteams). Fixes #1104 (#1105
- whatsapp: Update Rhymen/go-whatsapp. Fixes #1107 (#1109) (make whatsapp working again)
- discord: Add an ID cache (discord). Fixes #1106 (#1111) (fix delete/edits with webhooks)
# v1.17.3
## Enhancements
- xmpp: Implement User Avatar spoofing of XMPP users #1090
- rocketchat: Relay Joins/Topic changes in RocketChat bridge (#1085)
- irc: Add JoinDelay option (irc). Fixes #1084 (#1098)
- slack: Clip too long messages on 3000 length (slack). Fixes #1081 (#1102)
## Bugfix
- general: Fix the behavior of ShowTopicChange and SyncTopic (#1086)
- slack: Prevent image/message looping (slack). Fixes #1088 (#1096)
- whatsapp: Ignore non-critical errors (whatsapp). Fixes #1094 (#1100)
- irc: Add extra space before colon in attachments (irc). Fixes #1089 (#1101)
This release couldn't exist without the following contributors:
@42wim, @ldruschk, @qaisjp, @Polynomdivision
# v1.17.2
## Enhancements
- slack: Update vendor slack-go/slack (#1068)
- general: Update vendor d5/tengo (#1066)
- general: Clarify terminology used in mapping group chat IDs to channels in config (#1079)
## Bugfix
- whatsapp: Update Rhymen/go-whatsapp vendor and whatsapp version (#1078). Fixes Media upload #1074
- whatsapp: Reset start timestamp on reconnect (whatsapp). Fixes #1059 (#1064)
This release couldn't exist without the following contributors:
@42wim, @jheiselman
# v1.17.1
## Enhancements
- docker: Remove build dependencies from final image (multistage build) #1057
## Bugfix
- general: Don't transmit typing events from ourselves #1056
- general: Add support for build tags #1054
- discord: Strip extra info from emotes (discord) #1052
- msteams: fix macos build: Update vendor yaegashi/msgraph.go to v0.1.2 #1036
- whatsapp: Update client version whatsapp. Fixes #1061 #1062
This release couldn't exist without the following contributors:
@awigen, @qaisjp, @42wim
# v1.17.0
## New features
- msteams: new protocol added. Add initial Microsoft Teams support #967
See https://github.com/42wim/matterbridge/wiki/MS-Teams-setup for a complete walkthrough
- discord: Add ability to procure avatars from the destination bridge #1000
- matrix: Add support for avatars from matrix. #1007
- general: support JSON and YAML config formats #1045
## Enhancements
- discord: Check only bridged channels for PermManageWebhooks #1001
- irc: Be less lossy when throttling IRC messages #1004
- keybase: updated library #1002, #1019
- matrix: Rebase gomatrix vendor with upstream #1006
- slack: Use upstream slack-go/slack again #1018
- slack: Ignore ConnectingEvent #1041
- slack: use blocks not attachments #1048
- sshchat: Update vendor shazow/ssh-chat #1029
- telegram: added markdownv2 mode for telegram #1037
- whatsapp: Implement basic reconnect (whatsapp). Fixes #987 #1003
## Bugfix
- discord: Fix webhook permission checks sometimes failing #1043
- discord: Fix #1027: warning when handling inbound webhooks #1044
- discord: Fix duplicate separator on empty description/url (discord) #1035
- matrix: Fix issue with underscores in links #999
- slack: Fix #1039: messages sent to Slack being synced back #1046
- telegram: Make avatars download work with mediaserverdownload (telegram). Fixes #920
This release couldn't exist without the following contributors:
@qaisjp, @jakubgs, @burner1024, @notpushkin, @MartijnBraam, @42wim
# v1.16.5
- Fix version bump
# v1.16.4
## New features
- whatsapp: Add support for WhatsApp media (jpeg/png/gif) bridging (#974)
- telegram: Add QuoteLengthLimit option (telegram) fixes #963 (#985)
- telegram: Add DisableWebPagePreview option (telegram). Closes #980 (#994)
## Enhancements
- general: update dependencies
- tengo: update to tengo v2
- general: Add Docker Compose configuration (#990)
## Bugfix
- general: Fail with message instead of panic. #988 (#991)
- telegram: Add extra mimetypes to docker image. Fixes #969
- discord: Fix channel ID problem with multiple gateways (discord). Fixes #953 (#977)
- discord: Show file comment in webhook if normal message is empty (discord). Fixes #962 (#995)
- matrix: Fix parsing issues - Disable smartypants in markdown parser. Fixes #989, #983 (#993)
- sshchat: Fix duplicated messages (sshchat). Fixes #950 (#996)
This release couldn't exist without the following contributors:
@jwflory, @42wim, @pbek, @Humorhenker, @c0ncord2, @glazzara
# v1.16.3
## Bugfix
- slack: Fix issues with ratelimiting #959
- mattermost: Fix bug when using webhookURL and login/token together #960
# v1.16.2
## New features
- keybase: Add support for receiving attachments (keybase) (#923)
## Enhancements
- general: Switch to new emoji library kyokomi/emoji (#948)
- general: Update markdown parsing library to github.com/gomarkdown/markdown (#944)
- ssh-chat: Update shazow/ssh-chat dependency (#947)
## Bugfix
- slack: Fix issues with the slack block kit API #937 (#943).
This release couldn't exist without the following contributors:
@42wim, @bmpickford, @goncalor
# v1.16.1
## New features
* rocketchat: add token support #892
* matrix: Add support for uploading application/x and audio/x (matrix). #929
## Enhancements
* general: Do configuration validation on start-up. Fixes #888
* general: updated vendored libraries (discord/whatsapp) #932
* discord: user typing messages #914
* slack: Convert slack bold/strike to correct markdown (slack). Fixes #918
## Bugfix
* discord: fix Failed to fetch information for members message. #894
* discord: remove obsolete file upload links (discord). #931
* slack: suppress unhandled HelloEvent message #913
* mattermost: Fix panic on WebhookURL only setting (mattermost). #917
* matrix: fix corrupted links between slack and matrix #924
This release couldn't exist without the following contributors:
@qaisjp, @hramrach, @42wim
# v1.16.0
## New features
* keybase: new protocol added. Add initial Keybase Chat support #877 Thanks to @hyperobject
* discord: Support webhook files in discord #872
## Enhancements
* general: update dependencies
## Bugfix
* discord: Underscores from Discord don't arrive correctly #864
* xmpp: Fix possible panic at startup of the XMPP bridge #869
* mattermost: Make getChannelIdTeam behave like GetChannelId for groups (mattermost) #873
This release couldn't exist without the following contributors:
@hyperobject, @42wim, @bucko909, @MOZGIII
# v1.15.1 # v1.15.1
## New features
* discord: Support webhook message deletions (discord) (#853)
## Enhancements ## Enhancements
* discord: Support bulk deletions #851 * discord: Support bulk deletions #851
* discord: Support channels in categories #863 (use category/channel. See matterbridge.toml.sample for more info) * discord: Support channels in categories #863 (use category/channel. See matterbridge.toml.sample for more info)
* mattermost: Add an option to skip the Mattermost server version check #849 * mattermost: Add an option to skip the Mattermost server version check #849
## Bugfix ## Bugfix
* xmpp: fix segfault when disconnected/reconnected #856 * xmpp: fix segfault when disconnected/reconnected #856
* telegram: fix panic in handleEntities #858 * telegram: fix panic in handleEntities #858

30
ci/bintray.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -u -e -x -o pipefail
go version | grep go1.12 || exit
VERSION=$(git describe --tags)
mkdir ci/binaries
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-amd64
GOOS=linux GOARCH=arm go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-arm
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-darwin-amd64
cd ci
cat > deploy.json <<EOF
{
"package": {
"name": "Matterbridge",
"repo": "nightly",
"subject": "42wim"
},
"version": {
"name": "$VERSION"
},
"files":
[
{"includePattern": "ci/binaries/(.*)", "uploadPattern":"\$1"}
],
"publish": true
}
EOF

17
ci/lint.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -u -e -x -o pipefail
if [[ -n "${GOLANGCI_VERSION-}" ]]; then
# Retrieve the golangci-lint linter binary.
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION}
fi
# Run the linter.
golangci-lint run
if [[ "${GO111MODULE-off}" == "on" ]]; then
# If Go modules are active then check that dependencies are correctly maintained.
go mod tidy
go mod vendor
git diff --exit-code --quiet || (echo "Please run 'go mod tidy' to clean up the 'go.mod' and 'go.sum' files."; false)
fi

17
ci/test.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -u -e -x -o pipefail
if [[ -n "${REPORT_COVERAGE+cover}" ]]; then
# Retrieve and prepare CodeClimate's test coverage reporter.
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
chmod +x ./cc-test-reporter
./cc-test-reporter before-build
fi
# Run all the tests with the race detector and generate coverage.
go test -v -race -coverprofile c.out ./...
if [[ -n "${REPORT_COVERAGE+cover}" && "${TRAVIS_SECURE_ENV_VARS}" == "true" ]]; then
# Upload test coverage to CodeClimate.
./cc-test-reporter after-build
fi

View File

@@ -1,10 +0,0 @@
text := import("text")
// if we're not sending to a discord bridge,
// then convert custom emoji tags into url's
if (inProtocol == "discord" && outProtocol != "discord") {
rePNG := text.re_compile(`<:.*?:([0-9]+)>`)
msgText=rePNG.replace(msgText,"https://cdn.discordapp.com/emojis/$1.png")
reGIF := text.re_compile(`<a:.*?:([0-9]+)>`)
msgText=reGIF.replace(msgText,"https://cdn.discordapp.com/emojis/$1.gif")
}

View File

@@ -1,14 +0,0 @@
// See https://github.com/42wim/matterbridge/issues/881
// Generates a colored nick for each msgUsername, with example to filter specific codes
text := import("text")
fmt := import("fmt")
if outProtocol == "irc" {
// generate a color for a nick, make sure it isn't 0 or 15
colorCode := len(msgUsername)+bytes(msgUsername)[0]%14 + 2
// example if we want to use colorCode 3 when we have calculated colorcode 14
if colorCode == 14 {
colorCode = 3
}
msgUsername=fmt.sprintf("\x03%02d%s\x0F", colorCode, msgUsername)
}

View File

@@ -1,11 +0,0 @@
// +build !noapi
package bridgemap
import (
"github.com/42wim/matterbridge/bridge/api"
)
func init() {
FullMap["api"] = api.New
}

View File

@@ -1,12 +0,0 @@
// +build !nodiscord
package bridgemap
import (
bdiscord "github.com/42wim/matterbridge/bridge/discord"
)
func init() {
FullMap["discord"] = bdiscord.New
UserTypingSupport["discord"] = struct{}{}
}

View File

@@ -1,11 +0,0 @@
// +build !nogitter
package bridgemap
import (
bgitter "github.com/42wim/matterbridge/bridge/gitter"
)
func init() {
FullMap["gitter"] = bgitter.New
}

View File

@@ -1,11 +0,0 @@
// +build !noirc
package bridgemap
import (
birc "github.com/42wim/matterbridge/bridge/irc"
)
func init() {
FullMap["irc"] = birc.New
}

View File

@@ -1,11 +0,0 @@
// +build !nokeybase
package bridgemap
import (
bkeybase "github.com/42wim/matterbridge/bridge/keybase"
)
func init() {
FullMap["keybase"] = bkeybase.New
}

View File

@@ -1,11 +0,0 @@
// +build !nomatrix
package bridgemap
import (
bmatrix "github.com/42wim/matterbridge/bridge/matrix"
)
func init() {
FullMap["matrix"] = bmatrix.New
}

View File

@@ -1,11 +0,0 @@
// +build !nomattermost
package bridgemap
import (
bmattermost "github.com/42wim/matterbridge/bridge/mattermost"
)
func init() {
FullMap["mattermost"] = bmattermost.New
}

View File

@@ -1,11 +0,0 @@
// +build !nomsteams
package bridgemap
import (
bmsteams "github.com/42wim/matterbridge/bridge/msteams"
)
func init() {
FullMap["msteams"] = bmsteams.New
}

View File

@@ -1,11 +0,0 @@
// +build !nomumble
package bridgemap
import (
bmumble "github.com/42wim/matterbridge/bridge/mumble"
)
func init() {
FullMap["mumble"] = bmumble.New
}

View File

@@ -1,11 +0,0 @@
// +build !nonctalk
package bridgemap
import (
btalk "github.com/42wim/matterbridge/bridge/nctalk"
)
func init() {
FullMap["nctalk"] = btalk.New
}

View File

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

View File

@@ -1,11 +0,0 @@
// +build !norocketchat
package bridgemap
import (
brocketchat "github.com/42wim/matterbridge/bridge/rocketchat"
)
func init() {
FullMap["rocketchat"] = brocketchat.New
}

View File

@@ -1,13 +0,0 @@
// +build !noslack
package bridgemap
import (
bslack "github.com/42wim/matterbridge/bridge/slack"
)
func init() {
FullMap["slack-legacy"] = bslack.NewLegacy
FullMap["slack"] = bslack.New
UserTypingSupport["slack"] = struct{}{}
}

View File

@@ -1,11 +0,0 @@
// +build !nosshchat
package bridgemap
import (
bsshchat "github.com/42wim/matterbridge/bridge/sshchat"
)
func init() {
FullMap["sshchat"] = bsshchat.New
}

View File

@@ -1,11 +0,0 @@
// +build !nosteam
package bridgemap
import (
bsteam "github.com/42wim/matterbridge/bridge/steam"
)
func init() {
FullMap["steam"] = bsteam.New
}

View File

@@ -1,11 +0,0 @@
// +build !notelegram
package bridgemap
import (
btelegram "github.com/42wim/matterbridge/bridge/telegram"
)
func init() {
FullMap["telegram"] = btelegram.New
}

View File

@@ -1,11 +0,0 @@
// +build !nowhatsapp
package bridgemap
import (
bwhatsapp "github.com/42wim/matterbridge/bridge/whatsapp"
)
func init() {
FullMap["whatsapp"] = bwhatsapp.New
}

View File

@@ -1,11 +0,0 @@
// +build !noxmpp
package bridgemap
import (
bxmpp "github.com/42wim/matterbridge/bridge/xmpp"
)
func init() {
FullMap["xmpp"] = bxmpp.New
}

View File

@@ -1,11 +0,0 @@
// +build !nozulip
package bridgemap
import (
bzulip "github.com/42wim/matterbridge/bridge/zulip"
)
func init() {
FullMap["zulip"] = bzulip.New
}

View File

@@ -1,7 +1,6 @@
package gateway package gateway
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"regexp" "regexp"
@@ -11,10 +10,10 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/internal" "github.com/42wim/matterbridge/internal"
"github.com/d5/tengo/v2" "github.com/d5/tengo/script"
"github.com/d5/tengo/v2/stdlib" "github.com/d5/tengo/stdlib"
lru "github.com/hashicorp/golang-lru" lru "github.com/hashicorp/golang-lru"
"github.com/matterbridge/emoji" "github.com/peterhellberg/emojilib"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -86,7 +85,6 @@ func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
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 {
gw.checkConfig(cfg)
br = bridge.New(cfg) br = bridge.New(cfg)
br.Config = gw.Router.Config br.Config = gw.Router.Config
br.General = &gw.BridgeValues().General br.General = &gw.BridgeValues().General
@@ -106,19 +104,6 @@ func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
return nil return nil
} }
func (gw *Gateway) checkConfig(cfg *config.Bridge) {
match := false
for _, key := range gw.Router.Config.Viper().AllKeys() {
if strings.HasPrefix(key, strings.ToLower(cfg.Account)) {
match = true
break
}
}
if !match {
gw.logger.Fatalf("Account %s defined in gateway %s but no configuration found, exiting.", cfg.Account, gw.Name)
}
}
// AddConfig associates a new configuration with the gateway object. // 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
@@ -307,6 +292,8 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
} }
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string { func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string {
br := gw.Bridges[msg.Account]
msg.Protocol = br.Protocol
if dest.GetBool("StripNick") { if dest.GetBool("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, "")
@@ -314,7 +301,6 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri
nick := dest.GetString("RemoteNickFormat") nick := dest.GetString("RemoteNickFormat")
// loop to replace nicks // loop to replace nicks
br := gw.Bridges[msg.Account]
for _, outer := range br.GetStringSlice2D("ReplaceNicks") { for _, outer := range br.GetStringSlice2D("ReplaceNicks") {
search := outer[0] search := outer[0]
replace := outer[1] replace := outer[1]
@@ -364,28 +350,15 @@ 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 gw.BridgeValues().General.TengoModifyMessage != "" { if err := modifyMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil {
gw.logger.Warnf("General TengoModifyMessage=%s is deprecated and will be removed in v1.20.0, please move to Tengo InMessage=%s", gw.BridgeValues().General.TengoModifyMessage, gw.BridgeValues().General.TengoModifyMessage)
}
if err := modifyInMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil {
gw.logger.Errorf("TengoModifyMessage failed: %s", err) gw.logger.Errorf("TengoModifyMessage failed: %s", err)
} }
if err := modifyMessageTengo(gw.BridgeValues().Tengo.Message, msg); err != nil {
inMessage := gw.BridgeValues().Tengo.InMessage
if inMessage == "" {
inMessage = gw.BridgeValues().Tengo.Message
if inMessage != "" {
gw.logger.Warnf("Tengo Message=%s is deprecated and will be removed in v1.20.0, please move to Tengo InMessage=%s", inMessage, inMessage)
}
}
if err := modifyInMessageTengo(inMessage, msg); err != nil {
gw.logger.Errorf("Tengo.Message failed: %s", err) gw.logger.Errorf("Tengo.Message failed: %s", err)
} }
// replace :emoji: to unicode // replace :emoji: to unicode
msg.Text = emoji.Sprint(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
@@ -431,9 +404,8 @@ func (gw *Gateway) SendMessage(
} }
// Too noisy to log like other events // Too noisy to log like other events
debugSendMessage := ""
if msg.Event != config.EventUserTyping { if msg.Event != config.EventUserTyping {
debugSendMessage = fmt.Sprintf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name) 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.Channel = channel.Name
@@ -458,29 +430,17 @@ func (gw *Gateway) SendMessage(
msg.ParentID = "msg-parent-not-found" msg.ParentID = "msg-parent-not-found"
} }
drop, err := gw.modifyOutMessageTengo(rmsg, &msg, dest) err := gw.modifySendMessageTengo(rmsg, &msg, dest)
if err != nil { if err != nil {
gw.logger.Errorf("modifySendMessageTengo: %s", err) gw.logger.Errorf("modifySendMessageTengo: %s", err)
} }
if drop {
gw.logger.Debugf("=> Tengo dropping %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name)
return "", nil
}
if debugSendMessage != "" {
gw.logger.Debug(debugSendMessage)
}
// if we are using mattermost plugin account, send messages to MattermostPlugin channel // if we are using mattermost plugin account, send messages to MattermostPlugin channel
// that can be picked up by the mattermost matterbridge plugin // that can be picked up by the mattermost matterbridge plugin
if dest.Account == "mattermost.plugin" { if dest.Account == "mattermost.plugin" {
gw.Router.MattermostPlugin <- msg gw.Router.MattermostPlugin <- msg
} }
defer func(t time.Time) {
gw.logger.Debugf("=> Send from %s (%s) to %s (%s) took %s", msg.Account, rmsg.Channel, dest.Account, channel.Name, time.Since(t))
}(time.Now())
mID, err := dest.Send(msg) mID, err := dest.Send(msg)
if err != nil { if err != nil {
return mID, err return mID, err
@@ -532,7 +492,7 @@ func getProtocol(msg *config.Message) string {
return p[0] return p[0]
} }
func modifyInMessageTengo(filename string, msg *config.Message) error { func modifyMessageTengo(filename string, msg *config.Message) error {
if filename == "" { if filename == "" {
return nil return nil
} }
@@ -540,7 +500,7 @@ func modifyInMessageTengo(filename string, msg *config.Message) error {
if err != nil { if err != nil {
return err return err
} }
s := tengo.NewScript(res) s := script.New(res)
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("msgText", msg.Text) _ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username) _ = s.Add("msgUsername", msg.Username)
@@ -567,7 +527,7 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) (
if err != nil { if err != nil {
return "", err return "", err
} }
s := tengo.NewScript(res) s := script.New(res)
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("result", "") _ = s.Add("result", "")
_ = s.Add("msgText", msg.Text) _ = s.Add("msgText", msg.Text)
@@ -591,28 +551,22 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) (
return c.Get("result").String(), nil return c.Get("result").String(), nil
} }
func (gw *Gateway) modifyOutMessageTengo(origmsg *config.Message, msg *config.Message, br *bridge.Bridge) (bool, error) { func (gw *Gateway) modifySendMessageTengo(origmsg *config.Message, msg *config.Message, br *bridge.Bridge) error {
filename := gw.BridgeValues().Tengo.OutMessage filename := gw.BridgeValues().Tengo.OutMessage
var ( var res []byte
res []byte var err error
err error
drop bool
)
if filename == "" { if filename == "" {
res, err = internal.Asset("tengo/outmessage.tengo") res, err = internal.Asset("tengo/outmessage.tengo")
if err != nil { if err != nil {
return drop, err return err
} }
} else { } else {
res, err = ioutil.ReadFile(filename) res, err = ioutil.ReadFile(filename)
if err != nil { if err != nil {
return drop, err return err
} }
} }
s := script.New(res)
s := tengo.NewScript(res)
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("inAccount", origmsg.Account) _ = s.Add("inAccount", origmsg.Account)
_ = s.Add("inProtocol", origmsg.Protocol) _ = s.Add("inProtocol", origmsg.Protocol)
@@ -626,19 +580,14 @@ func (gw *Gateway) modifyOutMessageTengo(origmsg *config.Message, msg *config.Me
_ = s.Add("outEvent", msg.Event) _ = s.Add("outEvent", msg.Event)
_ = s.Add("msgText", msg.Text) _ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username) _ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgDrop", drop)
c, err := s.Compile() c, err := s.Compile()
if err != nil { if err != nil {
return drop, err return err
} }
if err := c.Run(); err != nil { if err := c.Run(); err != nil {
return drop, err return err
} }
drop = c.Get("msgDrop").Bool()
msg.Text = c.Get("msgText").String() msg.Text = c.Get("msgText").String()
msg.Username = c.Get("msgUsername").String() msg.Username = c.Get("msgUsername").String()
return nil
return drop, nil
} }

View File

@@ -15,15 +15,10 @@ import (
var testconfig = []byte(` var testconfig = []byte(`
[irc.freenode] [irc.freenode]
server=""
[mattermost.test] [mattermost.test]
server=""
[gitter.42wim] [gitter.42wim]
server=""
[discord.test] [discord.test]
server=""
[slack.test] [slack.test]
server=""
[[gateway]] [[gateway]]
name = "bridge1" name = "bridge1"
@@ -49,15 +44,10 @@ server=""
var testconfig2 = []byte(` var testconfig2 = []byte(`
[irc.freenode] [irc.freenode]
server=""
[mattermost.test] [mattermost.test]
server=""
[gitter.42wim] [gitter.42wim]
server=""
[discord.test] [discord.test]
server=""
[slack.test] [slack.test]
server=""
[[gateway]] [[gateway]]
name = "bridge1" name = "bridge1"
@@ -97,11 +87,8 @@ server=""
var testconfig3 = []byte(` var testconfig3 = []byte(`
[irc.zzz] [irc.zzz]
server=""
[telegram.zzz] [telegram.zzz]
server=""
[slack.zzz] [slack.zzz]
server=""
[[gateway]] [[gateway]]
name="bridge" name="bridge"
enable=true enable=true
@@ -189,6 +176,7 @@ func TestNewRouter(t *testing.T) {
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))
r = maketestRouter(testconfig2) r = maketestRouter(testconfig2)
assert.Equal(t, 2, len(r.Gateways)) assert.Equal(t, 2, len(r.Gateways))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges)) assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
@@ -533,7 +521,7 @@ func (s *ignoreTestSuite) TestIgnoreNicks() {
func BenchmarkTengo(b *testing.B) { func BenchmarkTengo(b *testing.B) {
msg := &config.Message{Username: "user", Text: "blah testing", Account: "protocol.account", Channel: "mychannel"} msg := &config.Message{Username: "user", Text: "blah testing", Account: "protocol.account", Channel: "mychannel"}
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
err := modifyInMessageTengo("bench.tengo", msg) err := modifyMessageTengo("bench.tengo", msg)
if err != nil { if err != nil {
return return
} }

View File

@@ -169,7 +169,7 @@ func (gw *Gateway) ignoreEvent(event string, dest *bridge.Bridge) bool {
switch event { switch event {
case config.EventAvatarDownload: case config.EventAvatarDownload:
// Avatar downloads are only relevant for telegram and mattermost for now // Avatar downloads are only relevant for telegram and mattermost for now
if dest.Protocol != "mattermost" && dest.Protocol != "telegram" && dest.Protocol != "xmpp" { if dest.Protocol != "mattermost" && dest.Protocol != "telegram" {
return true return true
} }
case config.EventJoinLeave: case config.EventJoinLeave:
@@ -179,7 +179,7 @@ func (gw *Gateway) ignoreEvent(event string, dest *bridge.Bridge) bool {
} }
case config.EventTopicChange: case config.EventTopicChange:
// only relay topic change when used in some way on other side // only relay topic change when used in some way on other side
if !dest.GetBool("ShowTopicChange") && !dest.GetBool("SyncTopic") { if dest.GetBool("ShowTopicChange") && dest.GetBool("SyncTopic") {
return true return true
} }
} }

View File

@@ -59,14 +59,8 @@ func NewRouter(rootLogger *logrus.Logger, cfg config.Config, bridgeMap map[strin
// between them. // between them.
func (r *Router) Start() error { func (r *Router) Start() error {
m := make(map[string]*bridge.Bridge) m := make(map[string]*bridge.Bridge)
if len(r.Gateways) == 0 {
return fmt.Errorf("no [[gateway]] configured. See https://github.com/42wim/matterbridge/wiki/How-to-create-your-config for more info")
}
for _, gw := range r.Gateways { for _, gw := range r.Gateways {
r.logger.Infof("Parsing gateway %s", gw.Name) r.logger.Infof("Parsing gateway %s", gw.Name)
if len(gw.Bridges) == 0 {
return fmt.Errorf("no bridges configured for gateway %s. See https://github.com/42wim/matterbridge/wiki/How-to-create-your-config for more info", gw.Name)
}
for _, br := range gw.Bridges { for _, br := range gw.Bridges {
m[br.Account] = br m[br.Account] = br
} }
@@ -132,9 +126,6 @@ func (r *Router) handleReceive() {
r.handleEventFailure(&msg) r.handleEventFailure(&msg)
r.handleEventRejoinChannels(&msg) r.handleEventRejoinChannels(&msg)
// Set message protocol based on the account it came from
msg.Protocol = r.getBridge(msg.Account).Protocol
filesHandled := false filesHandled := false
for _, gw := range r.Gateways { for _, gw := range r.Gateways {
// record all the message ID's of the different bridges // record all the message ID's of the different bridges

89
go.mod
View File

@@ -4,55 +4,66 @@ require (
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/Jeffail/gabs v1.1.1 // indirect github.com/Jeffail/gabs v1.1.1 // indirect
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560 github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329
github.com/Rhymen/go-whatsapp v0.1.2-0.20201122130733-6e5488ac98df github.com/Rhymen/go-whatsapp v0.0.2
github.com/d5/tengo/v2 v2.6.2 github.com/bwmarrin/discordgo v0.19.0
github.com/davecgh/go-spew v1.1.1 // github.com/bwmarrin/discordgo v0.19.0
github.com/fsnotify/fsnotify v1.4.9 github.com/d5/tengo v1.24.1
github.com/go-telegram-bot-api/telegram-bot-api v1.0.1-0.20200524105306-7434b0456e81 github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec
github.com/gomarkdown/markdown v0.0.0-20201113031856-722100d81a8e github.com/fsnotify/fsnotify v1.4.7
github.com/google/gops v0.3.13 github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible
github.com/google/gops v0.3.6
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect
github.com/gorilla/schema v1.2.0 github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect
github.com/gorilla/websocket v1.4.2 github.com/gorilla/schema v1.1.0
github.com/hashicorp/golang-lru v0.5.4 github.com/gorilla/websocket v1.4.0
github.com/jpillora/backoff v1.0.0 github.com/hashicorp/golang-lru v0.5.1
github.com/keybase/go-keybase-chat-bot v0.0.0-20200505163032-5cacf52379da github.com/hpcloud/tail v1.0.0 // indirect
github.com/labstack/echo/v4 v4.1.17 github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7
github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd github.com/labstack/echo/v4 v4.1.6
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20200411204219-d5c18ce75048 github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398
github.com/matterbridge/discordgo v0.22.0 github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect
github.com/matterbridge/go-xmpp v0.0.0-20200418225040-c8a3a57b4050 github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d
github.com/matterbridge/gozulipbot v0.0.0-20200820220548-be5824faa913 github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91
github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea
github.com/mattermost/mattermost-server/v5 v5.29.0 github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18
github.com/mattn/godown v0.0.0-20201027140031-2c7783b24de7 github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61
github.com/mattermost/mattermost-server v5.5.0+incompatible
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/missdeer/golib v1.0.4
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 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-20190326053356-55c9d7a5834c github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c
github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect
github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320
github.com/rs/xid v1.2.1 github.com/rs/xid v1.2.1
github.com/russross/blackfriday v1.5.2 github.com/russross/blackfriday v1.5.2
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca
github.com/shazow/ssh-chat v1.10.1 github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.4.2
github.com/slack-go/slack v0.7.2 github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect
github.com/spf13/viper v1.7.1 github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
github.com/stretchr/testify v1.6.1 github.com/spf13/viper v1.4.0
github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 github.com/stretchr/testify v1.3.0
github.com/writeas/go-strip-markdown v2.0.1+incompatible github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/yaegashi/msgraph.go v0.1.4
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2 github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a // indirect
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179 // indirect
gomod.garykim.dev/nc-talk v0.1.5 gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f
gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2 // indirect
layeh.com/gumble v0.0.0-20200818122324-146f9205029b gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe // indirect
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 // indirect
golang.org/x/image v0.0.0-20190616094056-33659d3de4f5
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
) )
go 1.15 replace github.com/bwmarrin/discordgo v0.19.0 => github.com/MOZGIII/discordgo v0.19.1-0.20190812115637-1e74183814f9

1296
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -12,14 +12,8 @@ text := import("text")
// start - strip irc colors // start - strip irc colors
// if we're not sending to an irc bridge we strip the IRC colors // if we're not sending to an irc bridge we strip the IRC colors
if inProtocol == "irc" && outProtocol != "irc" { if inProtocol == "irc" {
re := text.re_compile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`) re := text.re_compile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
msgText=re.replace(msgText,"") msgText=re.replace(msgText,"")
} }
// end - strip irc colors // end - strip irc colors
// strip custom emoji
if inProtocol == "discord" {
re := text.re_compile(`<a?(:.*?:)[0-9]+>`)
msgText=re.replace(msgText,"$1")
}

View File

@@ -4,7 +4,6 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"runtime"
"strings" "strings"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
@@ -16,7 +15,7 @@ import (
) )
var ( var (
version = "1.20.0" version = "1.15.2-dev"
githash string githash string
flagConfig = flag.String("conf", "matterbridge.toml", "config file") flagConfig = flag.String("conf", "matterbridge.toml", "config file")
@@ -51,15 +50,6 @@ func main() {
cfg := config.NewConfig(rootLogger, *flagConfig) cfg := config.NewConfig(rootLogger, *flagConfig)
cfg.BridgeValues().General.Debug = *flagDebug cfg.BridgeValues().General.Debug = *flagDebug
// if logging to a file, ensure it is closed when the program terminates
// nolint:errcheck
defer func() {
if f, ok := rootLogger.Out.(*os.File); ok {
f.Sync()
f.Close()
}
}()
r, err := gateway.NewRouter(rootLogger, cfg, bridgemap.FullMap) r, err := gateway.NewRouter(rootLogger, cfg, bridgemap.FullMap)
if err != nil { if err != nil {
logger.Fatalf("Starting gateway failed: %s", err) logger.Fatalf("Starting gateway failed: %s", err)
@@ -77,31 +67,17 @@ func setupLogger() *logrus.Logger {
Formatter: &prefixed.TextFormatter{ Formatter: &prefixed.TextFormatter{
PrefixPadding: 13, PrefixPadding: 13,
DisableColors: true, DisableColors: true,
FullTimestamp: true,
}, },
Level: logrus.InfoLevel, Level: logrus.InfoLevel,
} }
if *flagDebug || os.Getenv("DEBUG") == "1" { if *flagDebug || os.Getenv("DEBUG") == "1" {
logger.SetReportCaller(true)
logger.Formatter = &prefixed.TextFormatter{ logger.Formatter = &prefixed.TextFormatter{
PrefixPadding: 13, PrefixPadding: 13,
DisableColors: true, DisableColors: true,
FullTimestamp: false, FullTimestamp: false,
ForceFormatting: true,
CallerFormatter: func(function, file string) string {
return fmt.Sprintf(" [%s:%s]", function, file)
},
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
sp := strings.SplitAfter(f.File, "/matterbridge/")
filename := f.File
if len(sp) > 1 {
filename = sp[1]
} }
s := strings.Split(f.Function, ".")
funcName := s[len(s)-1]
return funcName, fmt.Sprintf("%s:%d", filename, f.Line)
},
}
logger.Level = logrus.DebugLevel logger.Level = logrus.DebugLevel
logger.WithFields(logrus.Fields{"prefix": "main"}).Info("Enabling debug logging.") logger.WithFields(logrus.Fields{"prefix": "main"}).Info("Enabling debug logging.")
} }

View File

@@ -103,16 +103,6 @@ ColorNicks=false
#OPTIONAL (default empty) #OPTIONAL (default empty)
RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"] RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"]
#PingDelay specifies how long to wait to send a ping to the irc server.
#You can use s for second, m for minute
#String
#OPTIONAL (default 1m)
PingDelay="1m"
#StripMarkdown strips markdown from messages
#OPTIONAL (default false)
StripMarkdown=false
#Nicks you want to ignore. #Nicks you want to ignore.
#Regular expressions supported #Regular expressions supported
#Messages from those users will not be sent to other bridges. #Messages from those users will not be sent to other bridges.
@@ -187,12 +177,6 @@ StripNick=false
#OPTIONAL (default false) #OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
#Delay in milliseconds between channel joins
#Only useful when you have a LOT of channels to join
#See https://github.com/42wim/matterbridge/issues/1084
#OPTIONAL (default 0)
JoinDelay=0
################################################################### ###################################################################
#XMPP section #XMPP section
################################################################### ###################################################################
@@ -227,10 +211,6 @@ Nick="xmppbot"
#OPTIONAL (default false) #OPTIONAL (default false)
SkipTLSVerify=true SkipTLSVerify=true
#Enable to use plaintext connection to your XMPP server.
#OPTIONAL (default false)
NoTLS=true
## RELOADABLE SETTINGS ## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file ## Settings below can be reloaded by editing the file
@@ -542,119 +522,6 @@ StripNick=false
#OPTIONAL (default false) #OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
###################################################################
#
# Keybase
# You should have a separate bridge account on Keybase
# (it also needs to be logged in on the system you're running the bridge on)
#
###################################################################
[keybase.myteam]
# RemoteNickFormat defines how remote users appear on this bridge
# See [general] config section for default options
RemoteNickFormat="{NICK} ({PROTOCOL}): "
# extra label that can be used in the RemoteNickFormat
# optional (default empty)
Label=""
# Your team on Keybase.
# The bot user MUST be a member of this team
# REQUIRED
Team="myteam"
###################################################################
# Microsoft teams section
# See https://github.com/42wim/matterbridge/wiki/MS-Teams-setup
###################################################################
[msteams.myteam]
# TenantID
# See https://github.com/42wim/matterbridge/wiki/MS-Teams-setup#get-necessary-ids-for-matterbridge
TenantID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# ClientID
# See https://github.com/42wim/matterbridge/wiki/MS-Teams-setup#get-necessary-ids-for-matterbridge
ClientID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# TeamID
# See https://github.com/42wim/matterbridge/wiki/MS-Teams-setup#get-necessary-ids-for-matterbridge
TeamID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge
#See [general] config section for default options
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
#It will strip other characters from the nick
#OPTIONAL (default false)
StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
#Opportunistically preserve threaded replies between bridges
#that support threading
#OPTIONAL (default false)
PreserveThreading=false
################################################################### ###################################################################
#slack section #slack section
################################################################### ###################################################################
@@ -817,121 +684,110 @@ ShowUserTyping=false
#In this example we use [discord.game] #In this example we use [discord.game]
#REQUIRED #REQUIRED
[discord.game] [discord.game]
# Token (REQUIRED) is the token to connect with Discord API #Token to connect with Discord API
#You can get your token by following the instructions on #You can get your token by following the instructions on
#https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token #https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token
#If you want roles/groups mentions to be shown with names instead of ID, you'll need to give your bot the "Manage Roles" permission. #If you want roles/groups mentions to be shown with names instead of ID, you'll need to give your bot the "Manage Roles" permission.
#REQUIRED
Token="Yourtokenhere" Token="Yourtokenhere"
# Server (REQUIRED) is the ID or name of the guild to connect to, selected from the guilds the bot has been invited to #REQUIRED
Server="yourservername" Server="yourservername"
## RELOADABLE SETTINGS ## RELOADABLE SETTINGS
## All settings below can be reloaded by editing the file. ## Settings below can be reloaded by editing the file
## They are also all optional.
# ShowEmbeds shows the title, description and URL of embedded messages (sent by other bots) #Shows title, description and URL of embedded messages (sent by other bots)
#OPTIONAL (default false)
ShowEmbeds=false ShowEmbeds=false
# UseLocalAvatar specifies source bridges for which an avatar should be 'guessed' when an incoming message has no avatar. #Shows the username instead of the server nickname
# This works by comparing the username of the message to an existing Discord user, and using the avatar of the Discord user. #OPTIONAL (default false)
#
# This only works if WebhookURL is set (AND the message has no avatar).
# Example: ["irc"]
UseLocalAvatar=[]
# UseUserName shows the username instead of the server nickname
UseUserName=false UseUserName=false
# UseDiscriminator appends the `#xxxx` discriminator when used with UseUserName #Show #xxxx discriminator with UseUserName
#OPTIONAL (default false)
UseDiscriminator=false UseDiscriminator=false
# WebhookURL sends messages in the style of puppets. #Specify WebhookURL. If given, will relay messages using the Webhook, which gives a better look to messages.
#This only works if you have one discord channel, if you have multiple discord channels you'll have to specify it in the gateway config #This only works if you have one discord channel, if you have multiple discord channels you'll have to specify it in the gateway config
# Example: "https://discordapp.com/api/webhooks/1234/abcd_xyzw" #OPTIONAL (default empty)
WebhookURL="" WebhookURL="Yourwebhooktokenhere"
# EditDisable disables sending of edits to other bridges #Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=false EditDisable=false
# EditSuffix specifies the message to be appended to every edited message #Message to be appended to every edited message
# Example: " (edited)" #OPTIONAL (default empty)
EditSuffix="" EditSuffix=" (edited)"
# IgnoreNicks mutes outgoing messages from certain users. #Nicks you want to ignore.
# Messages from these users will not be transmitted to other bridges. #Regular expressions supported
# Regular expressions are also supported. #Messages from those users will not be sent to other bridges.
# Example: "ircspammer1 ircspammer2" #OPTIONAL
IgnoreNicks="" IgnoreNicks="ircspammer1 ircspammer2"
# IgnoreMessages mutes outgoing messages of a certain format. #Messages you want to ignore.
# Messages matching this regular expression will not be transmitted sent to other bridges #Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info #See https://regex-golang.appspot.com/assets/html/index.html for more regex info
# #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
# Example that ignores messages starting with ~~ or messages containing badword: IgnoreMessages="^~~ badword"
# IgnoreMessages="^~~ badword"
IgnoreMessages=""
# ReplaceMessages replaces substrings of messages in outgoing messages. #messages you want to replace.
# Regular expressions are supported. #it replaces outgoing messages from the bridge.
# #so you need to place it by the sending bridge definition.
# Example that replaces 'cat' => 'dog' and 'sleep' => 'awake': #regular expressions supported
# ReplaceMessages=[ ["cat","dog"], ["sleep","awake"] ] #some examples:
# Example that replaces all digits with the letter 'X', so 'hello123' becomes 'helloXXX': #this replaces cat => dog and sleep => awake
# ReplaceMessages=[ ["[0-9]","X"] ] #replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
ReplaceMessages=[] #this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
# ReplaceNicks replaces substrings of usernames in outgoing messages. #nicks you want to replace.
# See the ReplaceMessages setting for examples. #see replacemessages for syntaxa
# Example: [ ["user--","user"] ] #optional (default empty)
ReplaceNicks=[] ReplaceNicks=[ ["user--","user"] ]
# ExtractNicks allows for interoperability with other bridge software by rewriting messages and extracting usernames. #Extractnicks is used to for example rewrite messages from other relaybots
# #See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
# Recommended reading: #some examples:
# - https://github.com/42wim/matterbridge/issues/466 #this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
# - https://github.com/42wim/matterbridge/issues/713
#
# This example translates the following message
# "Relaybot: <relayeduser> something interesting"
# into this message
# "relayeduser: something interesting"
# like so:
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ] #ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
# #you can use multiple entries for multiplebots
# This example translates the following message #this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
# "otherbot: (relayeduser) something else"
# into this message
# "relayeduser: something else"
# like so:
# ExtractNicks=[ [ "otherbot","\\((.*?)\\)\\s+" ] ]
#
# This example combines both of the above examples into one:
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ] #ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
# #OPTIONAL (default empty)
ExtractNicks=[] ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
# Label is as an extra identifier for use in the RemoteNickFormat setting. #extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label="" Label=""
# RemoteNickFormat formats how remote users appear on this bridge. #RemoteNickFormat defines how remote users appear on this bridge
# See the [general] config section for default options #See [general] config section for default options
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
# ShowJoinPart emits messages that show joins/parts from other bridges #Enable to show users joins/parts from other bridges
# Supported from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
# StripNick strips non-alphanumeric characters from nicknames. #StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
# Recommended reading: https://github.com/42wim/matterbridge/issues/285 #It will strip other characters from the nick
#OPTIONAL (default false)
StripNick=false StripNick=false
# ShowTopicChange emits messages that show topic/purpose updates from other bridges #Enable to show topic/purpose changes from other bridges
# Supported from the following bridges: slack #Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
# SyncTopic synchronises topic/purpose updates from other bridges #Enable to sync topic/purpose changes from other bridges
# Supported from the following bridges: slack #Only works syncing topic changes from slack bridge for now
#OPTIONAL (default false)
SyncTopic=false SyncTopic=false
################################################################### ###################################################################
@@ -952,17 +808,12 @@ Token="Yourtokenhere"
## Settings below can be reloaded by editing the file ## Settings below can be reloaded by editing the file
#OPTIONAL (default empty) #OPTIONAL (default empty)
#Supported formats are: #Supported formats are "HTML", "Markdown" and "HTMLNick"
#"HTML" https://core.telegram.org/bots/api#html-style #See https://core.telegram.org/bots/api#html-style
#"Markdown" https://core.telegram.org/bots/api#markdown-style - deprecated, doesn't display links with underscores correctly #See https://core.telegram.org/bots/api#markdown-style
#"MarkdownV2" https://core.telegram.org/bots/api#markdownv2-style #HTMLNick only allows HTML for the nick, the message itself will be html-escaped
#"HTMLNick" - only allows HTML for the nick, the message itself will be html-escaped
MessageFormat="" MessageFormat=""
#OPTIONAL (default false)
#Disables link previews for links in messages
DisableWebPagePreview=false
#If enabled use the "First Name" as username. If this is empty use the Username #If enabled use the "First Name" as username. If this is empty use the Username
#If disabled use the "Username" as username. If this is empty use the First Name #If disabled use the "Username" as username. If this is empty use the First Name
#If all names are empty, username will be "unknown" #If all names are empty, username will be "unknown"
@@ -979,10 +830,6 @@ UseInsecureURL=false
#OPTIONAL (default false) #OPTIONAL (default false)
QuoteDisable=false QuoteDisable=false
#Set the max. quoted length if 0 the whole message will be quoted
#OPTIONAL (default 0)
QuoteLengthLimit=0
#Format quoted/reply messages #Format quoted/reply messages
#OPTIONAL (default "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})") #OPTIONAL (default "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})")
QuoteFormat="{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" QuoteFormat="{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
@@ -1086,11 +933,6 @@ Server="https://yourrocketchatserver.domain.com:443"
#REQUIRED (when not using webhooks) #REQUIRED (when not using webhooks)
Login="yourlogin@domain.com" Login="yourlogin@domain.com"
Password="yourpass" Password="yourpass"
# When using access token set Login to the User ID associated with your token and Token to your token.
# When Token is set Password is ignored.
# Login="yOurUSerID"
# Token="YoUrUsER_toKEN"
#### Settings for webhook matterbridge. #### Settings for webhook matterbridge.
#USE DEDICATED BOT USER WHEN POSSIBLE! This allows you to use advanced features like message editing/deleting and uploads #USE DEDICATED BOT USER WHEN POSSIBLE! This allows you to use advanced features like message editing/deleting and uploads
@@ -1223,17 +1065,9 @@ Password="yourpass"
#OPTIONAL (default false) #OPTIONAL (default false)
NoHomeServerSuffix=false NoHomeServerSuffix=false
#Whether to disable sending of HTML content to matrix
#See https://github.com/42wim/matterbridge/issues/1022
#OPTIONAL (default false)
HTMLDisable=false
## RELOADABLE SETTINGS ## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file ## Settings below can be reloaded by editing the file
# UseUserName shows the username instead of the server nickname
UseUserName=false
#Whether to prefix messages from other bridges to matrix with the sender's nick. #Whether to prefix messages from other bridges to matrix with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the #Useful if username overrides for incoming webhooks isn't enabled on the
#matrix server. If you set PrefixMessagesWithNick to true, each message #matrix server. If you set PrefixMessagesWithNick to true, each message
@@ -1396,76 +1230,7 @@ StripNick=false
#OPTIONAL (default false) #OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
###################################################################
#
# NCTalk (Nextcloud Talk)
#
###################################################################
[nctalk.bridge]
# Url of your Nextcloud server
Server = "https://cloud.youdomain.me"
# Enable to not verify the certificate on your Nextcloud server.
# e.g. when using selfsigned certificates
# OPTIONAL (default false)
SkipTLSVerify=true
# Username of the bot
Login = "talkuser"
# Password of the bot
Password = "talkuserpass"
# Suffix for Guest Users
GuestSuffix = " (Guest)"
###################################################################
#
# Mumble
#
###################################################################
[mumble.bridge]
# Host and port of your Mumble server
Server = "mumble.yourdomain.me:64738"
# Nickname to log in as
Nick = "matterbridge"
# Some servers require a password
# OPTIONAL (default empty)
Password = "serverpasswordhere"
# User comment to set on the Mumble user, visible to other users.
# OPTIONAL (default empty)
UserComment="I am bridging text messages between this channel and #general on irc.yourdomain.me"
# Self-signed TLS client certificate + private key used to connect to
# Mumble. This is required if you want to register the matterbridge
# user on your Mumble server, so its nick becomes reserved.
# You can generate a keypair using e.g.
#
# openssl req -x509 -newkey rsa:2048 -nodes -days 10000 \
# -keyout mumble.key -out mumble.crt
#
# To actually register the matterbridege user, connect to Mumble as an
# admin, right click on the user and click "Register".
#
# OPTIONAL (default empty)
TLSClientCertificate="mumble.crt"
TLSClientKey="mumble.key"
# TLS CA certificate used to validate the Mumble server.
# OPTIONAL (defaults to Go system CA)
TLSCACertificate=mumble-ca.crt
# Enable to not verify the certificate on your Mumble server.
# e.g. when using selfsigned certificates
# OPTIONAL (default false)
SkipTLSVerify=false
################################################################### ###################################################################
# #
@@ -1607,8 +1372,6 @@ Buffer=1000
#Bearer token used for authentication #Bearer token used for authentication
#curl -H "Authorization: Bearer token" http://localhost:4242/api/messages #curl -H "Authorization: Bearer token" http://localhost:4242/api/messages
# https://github.com/vi/websocat
# websocat -H="Authorization: Bearer token" ws://127.0.0.1:4242/api/websocket
#OPTIONAL (no authorization if token is empty) #OPTIONAL (no authorization if token is empty)
Token="mytoken" Token="mytoken"
@@ -1688,14 +1451,6 @@ MediaDownloadBlacklist=[".html$",".htm$"]
#OPTIONAL (default false) #OPTIONAL (default false)
IgnoreFailureOnStart=false IgnoreFailureOnStart=false
#LogFile defines the location of a file to write logs into, rather
#than stdout.
#Logging will still happen on stdout if the file cannot be open for
#writing, or if the value is empty. Note that the log won't roll, so
#you might want to use logrotate(8) with this feature.
#OPTIONAL (default empty)
LogFile="/var/log/matterbridge.log"
################################################################### ###################################################################
#Tengo configuration #Tengo configuration
################################################################### ###################################################################
@@ -1733,9 +1488,7 @@ InMessage="example.tengo"
#outAccount, outProtocol, outChannel, outGateway, outEvent #outAccount, outProtocol, outChannel, outGateway, outEvent
# #
#read-write: #read-write:
#msgText, msgUsername, msgDrop #msgText, msgUsername
#
#msgDrop is a bool which is default false, when set true this message will be dropped
# #
#The script is reloaded on every message, so you can modify the script on the fly. #The script is reloaded on every message, so you can modify the script on the fly.
# #
@@ -1788,48 +1541,32 @@ enable=true
# REQUIRED # REQUIRED
account="irc.freenode" account="irc.freenode"
# The channel key in each gateway is mapped to a similar group chat ID on the chat platform # channel to connect on that account
# To find the group chat ID for different platforms, refer to the table below # How to specify them for the different bridges:
# #
# Platform | Identifier name | Example | Description # irc - #channel (# is required) (this needs to be lowercase!)
# ------------------------------------------------------------------------------------------------------------------------------------- # mattermost - channel (the channel name as seen in the URL, not the displayname)
# | channel | general | Do not include the # symbol # gitter - username/room
# discord | channel id | ID:123456789 | See https://github.com/42wim/matterbridge/issues/57 # xmpp - channel
# | category/channel | Media/gaming | Without # symbol. If you're using discord categories to group your channels # slack - channel (without the #)
# ------------------------------------------------------------------------------------------------------------------------------------- # - ID:C123456 (where C123456 is the channel ID) does not work with webhook
# gitter | username/room | general | As seen in the gitter.im URL # discord - channel (without the #)
# ------------------------------------------------------------------------------------------------------------------------------------- # - ID:123456789 (where 123456789 is the channel ID)
# hipchat | id_channel | example needed | See https://www.hipchat.com/account/xmpp for the correct channel # (https://github.com/42wim/matterbridge/issues/57)
# ------------------------------------------------------------------------------------------------------------------------------------- # - category/channel (without the #) if you're using discord categories to group your channels
# irc | channel | #general | The # symbol is required and should be lowercase! # telegram - chatid (a large negative number, eg -123456789)
# ------------------------------------------------------------------------------------------------------------------------------------- # see (https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau)
# mattermost | channel | general | This is the channel name as seen in the URL, not the display name # hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
# ------------------------------------------------------------------------------------------------------------------------------------- # rocketchat - #channel (# is required (also needed for private channels!)
# matrix | #channel:server | #yourchannel:matrix.org | Encrypted rooms are not supported in matrix # matrix - #channel:server (eg #yourchannel:matrix.org)
# ------------------------------------------------------------------------------------------------------------------------------------- # - encrypted rooms are not supported in matrix
# msteams | threadId | 19:82abcxx@thread.skype | You'll find the threadId in the URL # steam - chatid (a large number).
# ------------------------------------------------------------------------------------------------------------------------------------- # The number in the URL when you click "enter chat room" in the browser
# mumble | channel id | 42 | The channel ID, as shown in the channel's "Edit" window # whatsapp - 48111222333-123455678999@g.us A unique group JID;
# ------------------------------------------------------------------------------------------------------------------------------------- # if you specify an empty string bridge will list all the possibilities
# rocketchat | channel | #channel | # is required for private channels too # - "Group Name" if you specify a group name the bridge will hint its JID to specify
# ------------------------------------------------------------------------------------------------------------------------------------- # as group names might change in time and contain weird emoticons
# slack | channel name | general | Do not include the # symbol # zulip - stream/topic:topicname (without the #)
# | channel id | ID:C123456 | The underlying ID of a channel. This doesn't work with
# -------------------------------------------------------------------------------------------------------------------------------------
# steam | chatid | example needed | The number in the URL when you click "enter chat room" in the browser
# -------------------------------------------------------------------------------------------------------------------------------------
# nctalk | token | xs25tz5y | The token in the URL when you are in a chat. It will be the last part of the URL.
# -------------------------------------------------------------------------------------------------------------------------------------
# telegram | chatid | -123456789 | A large negative number. see https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau
# -------------------------------------------------------------------------------------------------------------------------------------
# whatsapp | group JID | 48111222333-123455678999@g.us | A unique group JID. If you specify an empty string, bridge will list all the possibilities
# | "Group Name" | "Family Chat" | if you specify a group name, the bridge will find hint the JID to specify. Names can change over time and are not stable.
# -------------------------------------------------------------------------------------------------------------------------------------
# xmpp | channel | general | The room name
# -------------------------------------------------------------------------------------------------------------------------------------
# zulip | stream/topic:topic | general/off-topic:food | Do not use the # when specifying a topic
# -------------------------------------------------------------------------------------------------------------------------------------
# #
# REQUIRED # REQUIRED
channel="#testing" channel="#testing"

View File

@@ -4,7 +4,7 @@ import (
"errors" "errors"
"strings" "strings"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/model"
) )
// GetChannels returns all channels we're members off // GetChannels returns all channels we're members off
@@ -36,16 +36,6 @@ func (m *MMClient) GetChannelHeader(channelId string) string { //nolint:golint
return "" return ""
} }
func getNormalisedName(channel *model.Channel) string {
if channel.Type == model.CHANNEL_GROUP {
// (deprecated in favor of ReplaceAll in go 1.12)
res := strings.Replace(channel.DisplayName, ", ", "-", -1) //nolint: gocritic
res = strings.Replace(res, " ", "_", -1) //nolint: gocritic
return res
}
return channel.Name
}
func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint
m.RLock() m.RLock()
defer m.RUnlock() defer m.RUnlock()
@@ -55,7 +45,13 @@ func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:go
for _, t := range m.OtherTeams { for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels, t.MoreChannels...) { for _, channel := range append(t.Channels, t.MoreChannels...) {
if getNormalisedName(channel) == name { if channel.Type == model.CHANNEL_GROUP {
res := strings.Replace(channel.DisplayName, ", ", "-", -1)
res = strings.Replace(res, " ", "_", -1)
if res == name {
return channel.Id
}
} else if channel.Name == name {
return channel.Id return channel.Id
} }
} }
@@ -67,7 +63,7 @@ func (m *MMClient) getChannelIdTeam(name string, teamId string) string { //nolin
for _, t := range m.OtherTeams { for _, t := range m.OtherTeams {
if t.Id == teamId { if t.Id == teamId {
for _, channel := range append(t.Channels, t.MoreChannels...) { for _, channel := range append(t.Channels, t.MoreChannels...) {
if getNormalisedName(channel) == name { if channel.Name == name {
return channel.Id return channel.Id
} }
} }
@@ -85,7 +81,12 @@ func (m *MMClient) GetChannelName(channelId string) string { //nolint:golint
} }
for _, channel := range append(t.Channels, t.MoreChannels...) { for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == channelId { if channel.Id == channelId {
return getNormalisedName(channel) if channel.Type == model.CHANNEL_GROUP {
res := strings.Replace(channel.DisplayName, ", ", "-", -1)
res = strings.Replace(res, " ", "_", -1)
return res
}
return channel.Name
} }
} }
} }
@@ -167,7 +168,7 @@ func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint
} }
func (m *MMClient) UpdateChannelsTeam(teamID string) error { func (m *MMClient) UpdateChannelsTeam(teamID string) error {
mmchannels, resp := m.Client.GetChannelsForTeamForUser(teamID, m.User.Id, false, "") mmchannels, resp := m.Client.GetChannelsForTeamForUser(teamID, m.User.Id, "")
if resp.Error != nil { if resp.Error != nil {
return errors.New(resp.Error.DetailedError) return errors.New(resp.Error.DetailedError)
} }

View File

@@ -13,7 +13,7 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/jpillora/backoff" "github.com/jpillora/backoff"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/model"
) )
func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error { func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error {
@@ -154,7 +154,7 @@ func (m *MMClient) initUser() error {
t := &Team{Team: team, Users: usermap, Id: team.Id} t := &Team{Team: team, Users: usermap, Id: team.Id}
mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, false, "") mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, "")
if resp.Error != nil { if resp.Error != nil {
return resp.Error return resp.Error
} }

View File

@@ -11,7 +11,7 @@ import (
lru "github.com/hashicorp/golang-lru" lru "github.com/hashicorp/golang-lru"
"github.com/jpillora/backoff" "github.com/jpillora/backoff"
prefixed "github.com/matterbridge/logrus-prefixed-formatter" prefixed "github.com/matterbridge/logrus-prefixed-formatter"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/model"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -69,7 +69,6 @@ type MMClient struct {
logger *logrus.Entry logger *logrus.Entry
rootLogger *logrus.Logger rootLogger *logrus.Logger
lruCache *lru.Cache lruCache *lru.Cache
allevents bool
} }
// New will instantiate a new Matterclient with the specified login details without connecting. // New will instantiate a new Matterclient with the specified login details without connecting.
@@ -120,10 +119,6 @@ func (m *MMClient) SetLogLevel(level string) {
} }
} }
func (m *MMClient) EnableAllEvents() {
m.allevents = true
}
// Login tries to connect the client with the loging details with which it was initialized. // 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
@@ -225,10 +220,6 @@ func (m *MMClient) WsReceiver() {
continue continue
} }
} }
if m.allevents {
m.MessageChan <- msg
continue
}
switch msg.Raw.Event { switch msg.Raw.Event {
case model.WEBSOCKET_EVENT_USER_ADDED, case model.WEBSOCKET_EVENT_USER_ADDED,
model.WEBSOCKET_EVENT_USER_REMOVED, model.WEBSOCKET_EVENT_USER_REMOVED,

View File

@@ -3,7 +3,7 @@ package matterclient
import ( import (
"strings" "strings"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/model"
) )
func (m *MMClient) parseActionPost(rmsg *Message) { func (m *MMClient) parseActionPost(rmsg *Message) {

View File

@@ -4,7 +4,7 @@ import (
"errors" "errors"
"time" "time"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/model"
) )
func (m *MMClient) GetNickName(userId string) string { //nolint:golint func (m *MMClient) GetNickName(userId string) string { //nolint:golint

View File

@@ -14,7 +14,7 @@ import (
"time" "time"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/slack-go/slack" "github.com/nlopes/slack"
) )
// OMessage for mattermost incoming webhook. (send to mattermost) // OMessage for mattermost incoming webhook. (send to mattermost)

View File

@@ -1,38 +0,0 @@
FROM alpine AS builder
COPY . /go/src/github.com/42wim/matterbridge
RUN apk add \
go \
git \
gcc \
musl-dev \
&& cd /go/src/github.com/42wim/matterbridge \
&& export GOPATH=/go \
&& go get \
&& go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
FROM alpine
RUN apk --no-cache add \
ca-certificates \
cairo \
libjpeg-turbo \
mailcap \
py3-webencodings \
python3 \
&& apk --no-cache add --virtual .compile \
gcc \
libffi-dev \
libjpeg-turbo-dev \
musl-dev \
py3-pip \
py3-wheel \
python3-dev \
zlib-dev \
&& pip3 install --no-cache-dir lottie[PNG] \
&& apk --no-cache del .compile
COPY --from=builder /bin/matterbridge /bin/matterbridge
RUN mkdir /etc/matterbridge \
&& touch /etc/matterbridge/matterbridge.toml \
&& ln -sf /matterbridge.toml /etc/matterbridge/matterbridge.toml
ENTRYPOINT ["/bin/matterbridge", "-conf", "/etc/matterbridge/matterbridge.toml"]

View File

@@ -1,6 +1,3 @@
[submodule "generator/SteamKit"] [submodule "generator/SteamKit"]
path = generator/SteamKit path = generator/SteamKit
url = https://github.com/SteamRE/SteamKit.git url = https://github.com/Philipp15b/SteamKit.git
[submodule "generator/Protobufs"]
path = generator/Protobufs
url = https://github.com/SteamDatabase/Protobufs.git

View File

@@ -45,10 +45,19 @@ Whether you want to develop your own Steam bot or directly work on go-steam itse
## Updating go-steam to a new SteamKit version ## Updating go-steam to a new SteamKit version
Go source code is generated with code in the `generator` directory. To update go-steam to a new version of SteamKit, do the following:
Look at `generator/README.md` for more information on how to use the generator.
Then, after generating new Go source files, update `go-steam` as necessary. go get github.com/golang/protobuf/protoc-gen-go/
git submodule init && git submodule update
cd generator
go run generator.go clean proto steamlang
Make sure that `$GOPATH/bin` / `protoc-gen-go` is in your `$PATH`. You'll also need [`protoc`](https://developers.google.com/protocol-buffers/docs/downloads), the protocol buffer compiler. At the moment, we use Protocol Buffers 2.6.1 with `proco-gen-go`-[2402d76](https://github.com/golang/protobuf/tree/2402d76f3d41f928c7902a765dfc872356dd3aad).
To compile the Steam Language files, you also need the [.NET Framework](https://www.microsoft.com/net/downloads)
on Windows or [mono](http://www.go-mono.com/mono-downloads/download.html) on other operating systems.
Apply the protocol changes where necessary.
## License ## License

View File

@@ -94,6 +94,9 @@ func (a *Auth) HandlePacket(packet *Packet) {
a.handleUpdateMachineAuth(packet) a.handleUpdateMachineAuth(packet)
case EMsg_ClientAccountInfo: case EMsg_ClientAccountInfo:
a.handleAccountInfo(packet) a.handleAccountInfo(packet)
case EMsg_ClientWalletInfoUpdate:
case EMsg_ClientRequestWebAPIAuthenticateUserNonceResponse:
case EMsg_ClientMarketingMessageUpdate:
} }
} }
@@ -119,7 +122,7 @@ func (a *Auth) handleLogOnResponse(packet *Packet) {
ExtendedResult: EResult(body.GetEresultExtended()), ExtendedResult: EResult(body.GetEresultExtended()),
OutOfGameSecsPerHeartbeat: body.GetOutOfGameHeartbeatSeconds(), OutOfGameSecsPerHeartbeat: body.GetOutOfGameHeartbeatSeconds(),
InGameSecsPerHeartbeat: body.GetInGameHeartbeatSeconds(), InGameSecsPerHeartbeat: body.GetInGameHeartbeatSeconds(),
PublicIp: body.GetDeprecatedPublicIp(), PublicIp: body.GetPublicIp(),
ServerTime: body.GetRtime32ServerTime(), ServerTime: body.GetRtime32ServerTime(),
AccountFlags: EAccountFlags(body.GetAccountFlags()), AccountFlags: EAccountFlags(body.GetAccountFlags()),
ClientSteamId: SteamId(body.GetClientSuppliedSteamid()), ClientSteamId: SteamId(body.GetClientSuppliedSteamid()),
@@ -127,7 +130,7 @@ func (a *Auth) handleLogOnResponse(packet *Packet) {
CellId: body.GetCellId(), CellId: body.GetCellId(),
CellIdPingThreshold: body.GetCellIdPingThreshold(), CellIdPingThreshold: body.GetCellIdPingThreshold(),
Steam2Ticket: body.GetSteam2Ticket(), Steam2Ticket: body.GetSteam2Ticket(),
UsePics: body.GetDeprecatedUsePics(), UsePics: body.GetUsePics(),
WebApiUserNonce: body.GetWebapiAuthenticateUserNonce(), WebApiUserNonce: body.GetWebapiAuthenticateUserNonce(),
IpCountryCode: body.GetIpCountryCode(), IpCountryCode: body.GetIpCountryCode(),
VanityUrl: body.GetVanityUrl(), VanityUrl: body.GetVanityUrl(),

View File

@@ -133,17 +133,11 @@ func (c *Client) Connected() bool {
// If you want to connect to a specific server, use `ConnectTo`. // If you want to connect to a specific server, use `ConnectTo`.
func (c *Client) Connect() *netutil.PortAddr { func (c *Client) Connect() *netutil.PortAddr {
var server *netutil.PortAddr var server *netutil.PortAddr
// try to initialize the directory cache
if !steamDirectoryCache.IsInitialized() {
_ = steamDirectoryCache.Initialize()
}
if steamDirectoryCache.IsInitialized() { if steamDirectoryCache.IsInitialized() {
server = steamDirectoryCache.GetRandomCM() server = steamDirectoryCache.GetRandomCM()
} else { } else {
server = GetRandomCM() server = GetRandomCM()
} }
c.ConnectTo(server) c.ConnectTo(server)
return server return server
} }

View File

@@ -1,8 +0,0 @@
module github.com/Philipp15b/go-steam
go 1.14
require (
github.com/davecgh/go-spew v1.1.1
github.com/golang/protobuf v1.4.2
)

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