4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-27 15:49:23 +00:00

Compare commits

..

11 Commits

5225 changed files with 286512 additions and 1076297 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,22 +0,0 @@
# Docs: <https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-dependency-updates>
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule: {interval: weekly}
reviewers: [42wim]
assignees: [42wim]
- package-ecosystem: github-actions
directory: /
schedule: {interval: weekly}
reviewers: [42wim]
assignees: [42wim]
- package-ecosystem: docker
directory: /
schedule: {interval: weekly}
reviewers: [42wim]
assignees: [42wim]

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,58 +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: latest
args: "-v --new-from-rev HEAD~5"
test-build-upload:
strategy:
matrix:
go-version: [1.17.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
stable: false
- 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)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.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.17')
uses: actions/upload-artifact@v2
with:
name: matterbridge-linux-64bit
path: output/lin
- name: Upload windows 64-bit
if: startsWith(matrix.go-version,'1.17')
uses: actions/upload-artifact@v2
with:
name: matterbridge-windows-64bit
path: output/win
- name: Upload darwin 64-bit
if: startsWith(matrix.go-version,'1.17')
uses: actions/upload-artifact@v2
with:
name: matterbridge-darwin-64bit
path: output/mac

View File

@ -1,68 +0,0 @@
name: docker
on:
push:
branches:
- 'master'
tags:
- 'v*'
pull_request:
branches:
- 'master'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: amd64,arm64
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: 42wim/matterbridge,ghcr.io/42wim/matterbridge
flavor: |
latest=true
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern=stable
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
-
name: Login to DockerHub
uses: docker/login-action@v1
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Log into registry ghcr.io
uses: docker/login-action@v1
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

9
.gitignore vendored
View File

@ -1,9 +0,0 @@
# Exclude matterbridge binary
/matterbridge
/matterbridge.exe
# Exclude configuration file
matterbridge.toml
# Exclude IDE Files
.vscode

View File

@ -7,7 +7,7 @@ run:
# concurrency: 4 # concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m # timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 5m deadline: 2m
# exit code when at least one issue was found, default is 1 # exit code when at least one issue was found, default is 1
issues-exit-code: 1 issues-exit-code: 1
@ -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
@ -91,6 +91,7 @@ linters-settings:
# Correct spellings using locale preferences for US or UK. # Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English. # Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'. # Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
lll: lll:
# max line length, lines longer will be reported. Default is 120. # max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option # '\t' is counted as 1 character by default, and can be changed with the tab-width option
@ -173,37 +174,8 @@ linters:
- lll - lll
- maligned - maligned
- prealloc - prealloc
- wsl
- gomnd
- godox
- goerr113
- testpackage
- godot
- interfacer
- goheader
- noctx
- gci
- errorlint
- nlreturn
- exhaustivestruct
- forbidigo
- wrapcheck
- varnamelen
- ireturn
- errorlint
- tparallel
- wrapcheck
- paralleltest
- makezero
- thelper
- cyclop
- revive
- importas
- gomoddirectives
- promlinter
- tagliatelle
- errname
- typecheck
# rules to deal with reported isues # rules to deal with reported isues
issues: issues:
# List of regexps of issue texts to exclude, empty list by default. # List of regexps of issue texts to exclude, empty list by default.

View File

@ -18,17 +18,10 @@ builds:
- arm - arm
- arm64 - arm64
- 386 - 386
goarm:
- 6
- 7
ldflags: ldflags:
- -s -w -X github.com/42wim/matterbridge/version.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:

55
.travis.yml Normal file
View File

@ -0,0 +1,55 @@
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.16.0"
- 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,14 +1,11 @@
FROM alpine AS builder FROM alpine:edge
ENTRYPOINT ["/bin/matterbridge"]
COPY . /go/src/matterbridge COPY . /go/src/github.com/42wim/matterbridge
RUN apk --no-cache add go git \ RUN apk update && apk add go git gcc musl-dev ca-certificates \
&& cd /go/src/matterbridge \ && cd /go/src/github.com/42wim/matterbridge \
&& CGO_ENABLED=0 go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge && export GOPATH=/go \
&& go get \
FROM alpine && go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge \
RUN apk --no-cache add ca-certificates mailcap && rm -rf /go \
COPY --from=builder /bin/matterbridge /bin/matterbridge && apk del --purge git go gcc musl-dev
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"]

349
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,185 +41,130 @@ 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](#installing)
- [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) * [Examples](#examples)
- [Building](#building) * [Running](#running)
- [Configuration](#configuration) * [Docker](#docker)
- [Basic configuration](#basic-configuration) * [Changelog](#changelog)
- [Settings](#settings) * [FAQ](#faq)
- [Advanced configuration](#advanced-configuration) * [Related projects](#related-projects)
- [Examples](#examples) * [Articles](#articles)
- [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing) * [Thanks](#thanks)
- [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general)
- [Running](#running)
- [Docker](#docker)
- [Systemd](#systemd)
- [Changelog](#changelog)
- [FAQ](#faq)
- [Related projects](#related-projects)
- [Articles / Tutorials](#articles--tutorials)
- [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/) * [Discord](https://discordapp.com)
- [Microsoft Teams](https://teams.microsoft.com) * [Telegram](https://telegram.org)
- [Mumble](https://www.mumble.info/) * [Hipchat](https://www.hipchat.com)
- [Nextcloud Talk](https://nextcloud.com/talk/) * [Rocket.chat](https://rocket.chat)
- [Rocket.chat](https://rocket.chat) * [Matrix](https://matrix.org)
- [Slack](https://slack.com) * [Steam](https://store.steampowered.com/)
- [Ssh-chat](https://github.com/shazow/ssh-chat) * [Twitch](https://twitch.tv)
- ~~[Steam](https://store.steampowered.com/)~~ * [Ssh-chat](https://github.com/shazow/ssh-chat)
- Not supported anymore, see [here](https://github.com/Philipp15b/go-steam/issues/94) for more info. * [WhatsApp](https://www.whatsapp.com/)
- [Telegram](https://telegram.org) * [Zulip](https://zulipchat.com)
- [Twitch](https://twitch.tv)
- [VK](https://vk.com/)
- [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/powerjungle/fbridge-asyncio) * [Facebook messenger](https://github.com/VictorNine/fbridge)
- [Facebook messenger](https://github.com/VictorNine/fbridge) * [Discourse](https://github.com/DeclanHoare/matterbabble)
- [Minecraft](https://github.com/elytra/MatterLink)
- [Minecraft](https://github.com/raws/mattercraft)
- [Minecraft](https://gitlab.com/Programie/MatterBukkit)
- [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)
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
### API ### API
The API is very basic at the moment.
The API is basic at the moment.
More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api). More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api).
Used by the projects below. Feel free to make a PR to add your project to this list. Used by the projects below. Feel free to make a PR to add your project to this list.
- [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Forge server chat, archived) * [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
- [MatterCraft](https://github.com/raws/mattercraft) (Matterbridge link for Minecraft Forge server chat) * [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
- [MatterBukkit](https://gitlab.com/Programie/MatterBukkit) (Matterbridge link for Minecraft Bukkit/Spigot server chat) * [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot) * [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
- [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support) * [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
- [fbridge-asyncio](https://github.com/powerjungle/fbridge-asyncio) (Facebook messenger support)
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger 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)
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
## 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]
- [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
## Installing / upgrading
### Binaries ### Binaries
* Latest stable release [v1.14.2](https://github.com/42wim/matterbridge/releases/latest)
- Latest stable release [v1.23.2](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). On \*nix platforms you may need to make the binary executable - you can do this by running `chmod a+x` on the binary (example: `chmod a+x matterbridge-1.20.0-linux-64bit`). After downloading (and making the binary executable, if necessary), follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
### Packages ### Packages
* [Overview](https://repology.org/metapackage/matterbridge/versions)
- [Overview](https://repology.org/metapackage/matterbridge/versions) ### Building
- [snap](https://snapcraft.io/matterbridge) 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).
- [scoop](https://github.com/42wim/scoop-bucket)
## Building After Go is setup, download matterbridge to your $GOPATH directory.
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest) ```
cd $GOPATH
If you really want to build from source, follow these instructions: go get github.com/42wim/matterbridge
Go 1.17+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed.
To install the latest stable run:
```bash
go install github.com/42wim/matterbridge
``` ```
To install the latest dev run: You should now have matterbridge binary in the bin directory:
```bash
go install github.com/42wim/matterbridge@master
``` ```
$ ls bin/
You should now have matterbridge binary in the ~/go/bin directory:
```bash
$ ls ~/go/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
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.libera] [irc.freenode]
Server="irc.libera.chat:6667" Server="irc.freenode.net:6667"
Nick="yourbotname" Nick="yourbotname"
[mattermost] [mattermost]
@ -238,7 +180,7 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for
name="mygateway" name="mygateway"
enable=true enable=true
[[gateway.inout]] [[gateway.inout]]
account="irc.libera" account="irc.freenode"
channel="#testing" channel="#testing"
[[gateway.inout]] [[gateway.inout]]
@ -247,7 +189,6 @@ enable=true
``` ```
#### Bridge slack (#general) - discord (general) #### Bridge slack (#general) - discord (general)
```toml ```toml
[slack] [slack]
[slack.test] [slack.test]
@ -279,7 +220,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")
@ -292,15 +233,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
### Systemd ```
Please take a look at the [Service Files page](https://github.com/42wim/matterbridge/wiki/Service-files) for more information.
## 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
@ -308,36 +246,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)
* [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig)
* [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
* [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
* [mattereddit](https://github.com/bonehurtingjuice/mattereddit)
* [matterlink](https://github.com/elytra/MatterLink)
* [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
* [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)
- [jwflory/ansible-role-matterbridge](https://galaxy.ansible.com/jwflory/matterbridge) (Ansible role to simplify deploying Matterbridge) ## Articles
- [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig) * [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
- [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer) * https://mattermost.com/blog/connect-irc-to-mattermost/
- [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku) * https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/
- [mattereddit](https://github.com/bonehurtingjuice/mattereddit) * https://blog.brightscout.com/top-10-mattermost-integrations/
- [matterlink](https://github.com/elytra/MatterLink) * http://bencey.co.nz/2018/09/17/bridge/
- [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost * https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot) * https://kopano.com/blog/matterbridge-bridging-mattermost-chat/
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) * https://www.stitcher.com/s/?eid=52382713
- [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge) * https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
- [nextcloud talk](https://github.com/nextcloud/talk_matterbridge) (Integrates matterbridge in Nextcloud Talk)
- [mattercraft](https://github.com/raws/mattercraft) (Minecraft bridge)
- [vs-matterbridge](https://github.com/NikkyAI/vs-matterbridge) (Vintage Story bridge)
## Articles / Tutorials
- [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
- <https://mattermost.com/blog/connect-irc-to-mattermost/>
- <https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/>
- <https://blog.brightscout.com/top-10-mattermost-integrations/>
- <https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/>
- <https://kopano.com/blog/matterbridge-bridging-mattermost-chat/>
- <https://www.stitcher.com/s/?eid=52382713>
- <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/>
- <https://userlinux.net/mattermost-and-matterbridge.html>
- <https://nextcloud.com/blog/bridging-chat-services-in-talk/>
- <https://minecraftchest1.wordpress.com/2021/06/05/how-to-install-and-setup-matterbridge/>
- Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc)
## Thanks ## Thanks
@ -349,43 +279,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>
- vk - <https://github.com/SevereCloud/vksdk>
- 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://web.libera.chat/#matterbridge [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"
@ -26,11 +24,8 @@ const (
EventAPIConnected = "api_connected" EventAPIConnected = "api_connected"
EventUserTyping = "user_typing" EventUserTyping = "user_typing"
EventGetChannelMembers = "get_channel_members" EventGetChannelMembers = "get_channel_members"
EventNoticeIRC = "notice_irc"
) )
const ParentIDNotFound = "msg-parent-not-found"
type Message struct { type Message struct {
Text string `json:"text"` Text string `json:"text"`
Channel string `json:"channel"` Channel string `json:"channel"`
@ -47,14 +42,6 @@ type Message struct {
Extra map[string][]interface{} Extra map[string][]interface{}
} }
func (m Message) ParentNotFound() bool {
return m.ParentID == ParentIDNotFound
}
func (m Message) ParentValid() bool {
return m.ParentID != "" && !m.ParentNotFound()
}
type FileInfo struct { type FileInfo struct {
Name string Name string
Data *[]byte Data *[]byte
@ -85,34 +72,27 @@ type ChannelMember struct {
type ChannelMembers []ChannelMember type ChannelMembers []ChannelMember
type Protocol struct { type Protocol struct {
AllowMention []string // discord
AuthCode string // steam AuthCode string // steam
BindAddress string // mattermost, slack // DEPRECATED BindAddress string // mattermost, slack // DEPRECATED
Buffer int // api Buffer int // api
Charset string // irc Charset string // irc
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
@ -120,7 +100,6 @@ type Protocol struct {
MessageQueue int // IRC, size of message queue for flood control MessageQueue int // IRC, size of message queue for flood control
MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping
Muc string // xmpp Muc string // xmpp
MxID string // matrix
Name string // all protocols Name string // all protocols
Nick string // all protocols Nick string // all protocols
NickFormatter string // mattermost, slack NickFormatter string // mattermost, slack
@ -130,48 +109,38 @@ 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
RealName string // IRC
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,matrix 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
ShowEmbeds bool // discord ShowEmbeds bool // discord
SkipTLSVerify bool // IRC, mattermost SkipTLSVerify bool // IRC, mattermost
SkipVersionCheck bool // mattermost
StripNick bool // all protocols StripNick bool // all protocols
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 Token string // gitter, slack, discord, api
TenantID string // msteams
Token string // gitter, slack, discord, api, matrix
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
UserName string // IRC
VerboseJoinPart bool // IRC
WebhookBindAddress string // mattermost, slack WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack WebhookURL string // mattermost, slack
} }
@ -197,13 +166,6 @@ type Gateway struct {
InOut []Bridge InOut []Bridge
} }
type Tengo struct {
InMessage string
Message string
RemoteNickFormat string
OutMessage string
}
type SameChannelGateway struct { type SameChannelGateway struct {
Name string Name string
Enable bool Enable bool
@ -227,18 +189,13 @@ 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
Gateway []Gateway Gateway []Gateway
SameChannelGateway []SameChannelGateway SameChannelGateway []SameChannelGateway
} }
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)
@ -264,17 +221,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
} }
@ -285,37 +232,25 @@ 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()
if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil { if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil {
logger.Fatalf("Failed to parse the configuration: %s", err) logger.Fatalf("Failed to parse the configuration: %#v", err)
} }
cfg := &BridgeValues{} cfg := &BridgeValues{}
if err := viper.Unmarshal(cfg); err != nil { if err := viper.Unmarshal(cfg); err != nil {
logger.Fatalf("Failed to load the configuration: %s", err) logger.Fatalf("Failed to load the configuration: %#v", err)
} }
return &config{ return &config{
logger: logger, logger: logger,
@ -328,16 +263,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()
@ -397,11 +322,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

@ -2,15 +2,15 @@ package bdiscord
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
"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/discord/transmitter"
"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,8 +21,11 @@ type Bdiscord struct {
c *discordgo.Session c *discordgo.Session
nick string nick string
userID string useChannelID bool
guildID string guildID string
webhookID string
webhookToken string
canEditWebhooks bool
channelsMutex sync.RWMutex channelsMutex sync.RWMutex
channels []*discordgo.Channel channels []*discordgo.Channel
@ -31,10 +34,6 @@ type Bdiscord struct {
membersMutex sync.RWMutex membersMutex sync.RWMutex
userMemberMap map[string]*discordgo.Member userMemberMap map[string]*discordgo.Member
nickMemberMap map[string]*discordgo.Member nickMemberMap map[string]*discordgo.Member
// Webhook specific logic
useAutoWebhooks bool
transmitter *transmitter.Transmitter
} }
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
@ -42,18 +41,23 @@ func New(cfg *bridge.Config) bridge.Bridger {
b.userMemberMap = make(map[string]*discordgo.Member) b.userMemberMap = make(map[string]*discordgo.Member)
b.nickMemberMap = make(map[string]*discordgo.Member) b.nickMemberMap = make(map[string]*discordgo.Member)
b.channelInfoMap = make(map[string]*config.ChannelInfo) b.channelInfoMap = make(map[string]*config.ChannelInfo)
if b.GetString("WebhookURL") != "" {
b.useAutoWebhooks = b.GetBool("AutoWebhooks") b.Log.Debug("Configuring Discord Incoming Webhook")
if b.useAutoWebhooks { b.webhookID, b.webhookToken = b.splitURL(b.GetString("WebhookURL"))
b.Log.Debug("Using automatic webhooks")
} }
return b return b
} }
func (b *Bdiscord) Connect() error { func (b *Bdiscord) Connect() error {
var err error var err error
var guildFound bool
token := b.GetString("Token") token := b.GetString("Token")
b.Log.Info("Connecting") b.Log.Info("Connecting")
if b.GetString("WebhookURL") == "" {
b.Log.Info("Connecting using token")
} else {
b.Log.Info("Connecting using webhookurl (for posting) and token")
}
if !strings.HasPrefix(b.GetString("Token"), "Bot ") { if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
token = "Bot " + b.GetString("Token") token = "Bot " + b.GetString("Token")
} }
@ -68,18 +72,11 @@ 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)
b.c.AddHandler(b.messageDeleteBulk)
b.c.AddHandler(b.memberAdd) b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove) b.c.AddHandler(b.memberRemove)
// Add privileged intent for guild member tracking. This is needed to track nicks
// for display names and @mention translation
b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
discordgo.IntentsGuildMembers)
err = b.c.Open() err = b.c.Open()
if err != nil { if err != nil {
return err return err
@ -94,108 +91,55 @@ 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
// Try and find this account's guild, and populate channels
b.channelsMutex.Lock() b.channelsMutex.Lock()
for _, guild := range guilds { for _, guild := range guilds {
// Skip, if the server name does not match the visible name or the ID if guild.Name == serverName || guild.ID == serverName {
if guild.Name != serverName && guild.ID != serverName {
continue
}
// Complain about an ambiguous Server setting. Two Discord servers could have the same title!
// For IDs, practically this will never happen. It would only trigger if some server's name is also an ID.
if b.guildID != "" {
return fmt.Errorf("found multiple Discord servers with the same name %#v, expected to see only one", serverName)
}
// Getting this guild's channel could result in a permission error
b.channels, err = b.c.GuildChannels(guild.ID) b.channels, err = b.c.GuildChannels(guild.ID)
if err != nil {
return fmt.Errorf("could not get %#v's channels: %w", b.GetString("Server"), err)
}
b.guildID = guild.ID b.guildID = guild.ID
guildFound = true
if err != nil {
break
}
}
} }
b.channelsMutex.Unlock() b.channelsMutex.Unlock()
if !guildFound {
// If we couldn't find a guild, we print extra debug information and return a nice error msg := fmt.Sprintf("Server \"%s\" not found", b.GetString("Server"))
if b.guildID == "" { err = errors.New(msg)
err = fmt.Errorf("could not find Discord server %#v", b.GetString("Server")) b.Log.Error(msg)
b.Log.Error(err.Error()) b.Log.Info("Possible values:")
// Print all of the possible server values
b.Log.Info("Possible server values:")
for _, guild := range guilds { for _, guild := range guilds {
b.Log.Infof("\t- Server=%#v # by name", guild.Name) b.Log.Infof("Server=\"%s\" # Server name", guild.Name)
b.Log.Infof("\t- Server=%#v # by ID", guild.ID) b.Log.Infof("Server=\"%s\" # Server ID", guild.ID)
}
} }
// If there are no results, we should say that
if len(guilds) == 0 {
b.Log.Info("\t- (none found)")
}
return err
}
// Legacy note: WebhookURL used to have an actual webhook URL that we would edit,
// but we stopped doing that due to Discord making rate limits more aggressive.
//
// Even older: the same WebhookURL used to be used by every channel, which is usually unexpected.
// This is no longer possible.
if b.GetString("WebhookURL") != "" {
message := "The global WebhookURL setting has been removed. "
message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. "
message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections."
b.Log.Errorln(message)
return fmt.Errorf("use of removed WebhookURL setting")
}
if b.GetInt("debuglevel") > 0 {
b.Log.Debug("enabling even more discord debug")
b.c.Debug = true
}
// Initialise webhook management
b.transmitter = transmitter.New(b.c, b.guildID, "matterbridge", b.useAutoWebhooks)
b.transmitter.Log = b.Log
var webhookChannelIDs []string
for _, channel := range b.Channels {
channelID := b.getChannelID(channel.Name) // note(qaisjp): this readlocks channelsMutex
// If a WebhookURL was not explicitly provided for this channel,
// there are two options: just a regular bot message (ugly) or this is should be webhook sent
if channel.Options.WebhookURL == "" {
// If it should be webhook sent, we should enforce this via the transmitter
if b.useAutoWebhooks {
webhookChannelIDs = append(webhookChannelIDs, channelID)
}
continue
}
whID, whToken, ok := b.splitURL(channel.Options.WebhookURL)
if !ok {
return fmt.Errorf("failed to parse WebhookURL %#v for channel %#v", channel.Options.WebhookURL, channel.ID)
}
b.transmitter.AddWebhook(channelID, &discordgo.Webhook{
ID: whID,
Token: whToken,
GuildID: b.guildID,
ChannelID: channelID,
})
}
if b.useAutoWebhooks {
err = b.transmitter.RefreshGuildWebhooks(webhookChannelIDs)
if err != nil { if err != nil {
b.Log.WithError(err).Println("transmitter could not refresh guild webhooks")
return err return err
} }
b.channelsMutex.RLock()
if b.GetString("WebhookURL") == "" {
for _, channel := range b.channels {
b.Log.Debugf("found channel %#v", channel)
} }
} else {
b.canEditWebhooks = true
for _, channel := range b.channels {
b.Log.Debugf("found channel %#v; verifying PermissionManageWebhooks", channel)
perms, permsErr := b.c.State.UserChannelPermissions(userinfo.ID, channel.ID)
manageWebhooks := discordgo.PermissionManageWebhooks
if permsErr != nil || perms&manageWebhooks != manageWebhooks {
b.Log.Warnf("Can't manage webhooks in channel \"%s\"", channel.Name)
b.canEditWebhooks = false
}
}
if b.canEditWebhooks {
b.Log.Info("Can manage webhooks; will edit channel for global webhook on send")
} else {
b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send")
}
}
b.channelsMutex.RUnlock()
// Obtaining guild members and initializing nickname mapping. // Obtaining guild members and initializing nickname mapping.
b.membersMutex.Lock() b.membersMutex.Lock()
@ -228,6 +172,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
} }
@ -239,36 +187,81 @@ 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 + "_"
} }
// Handle prefix hint for unthreaded messages. // use initial webhook configured for the entire Discord account
if msg.ParentNotFound() { isGlobalWebhook := true
msg.ParentID = "" wID := b.webhookID
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) wToken := b.webhookToken
// check if have a channel specific webhook
b.channelsMutex.RLock()
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
if ci.Options.WebhookURL != "" {
wID, wToken = b.splitURL(ci.Options.WebhookURL)
isGlobalWebhook = false
} }
}
b.channelsMutex.RUnlock()
// Use webhook to send the message // Use webhook to send the message
useWebhooks := b.shouldMessageUseWebhooks(&msg) if wID != "" {
if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" { // skip events
return b.handleEventWebhook(&msg, channelID) if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
return "", nil
}
b.Log.Debugf("Broadcasting using Webhook")
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
}
// skip empty messages
if msg.Text == "" {
return "", nil
} }
return b.handleEventBotUser(&msg, channelID) msg.Text = helper.ClipMessage(msg.Text, MessageLength)
msg.Text = b.replaceUserMentions(msg.Text)
// discord username must be [0..32] max
if len(msg.Username) > 32 {
msg.Username = msg.Username[0:32]
}
// if we have a global webhook for this Discord account, and permission
// to modify webhooks (previously verified), then set its channel to
// the message channel before using it
// TODO: this isn't necessary if the last message from this webhook was
// sent to the current channel
if isGlobalWebhook && b.canEditWebhooks {
b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel)
_, err := b.c.WebhookEdit(wID, "", "", channelID)
if err != nil {
b.Log.Errorf("Could not set webhook channel: %s", err)
return "", err
}
}
err := b.c.WebhookExecute(
wID,
wToken,
true,
&discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
})
return "", err
} }
// handleEventDirect handles events via the bot user
func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) {
b.Log.Debugf("Broadcasting using token (API)") b.Log.Debugf("Broadcasting using token (API)")
// Delete message // Delete message
@ -282,19 +275,19 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
// 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) {
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped")) rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength)
if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil { if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
b.Log.Errorf("Could not send message %#v: %s", rmsg, err) b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
} }
} }
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(msg, channelID) return b.handleUploadFile(&msg, channelID)
} }
} }
msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped")) msg.Text = helper.ClipMessage(msg.Text, MessageLength)
msg.Text = b.replaceUserMentions(msg.Text) msg.Text = b.replaceUserMentions(msg.Text)
// Edit message // Edit message
@ -303,26 +296,52 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
return msg.ID, err return msg.ID, err
} }
m := discordgo.MessageSend{
Content: msg.Username + msg.Text,
AllowedMentions: b.getAllowedMentions(),
}
if msg.ParentValid() {
m.Reference = &discordgo.MessageReference{
MessageID: msg.ParentID,
ChannelID: channelID,
GuildID: b.guildID,
}
}
// Post normal message // Post normal message
res, err := b.c.ChannelMessageSendComplex(channelID, &m) res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
if err != nil { if err != nil {
return "", err return "", err
} }
return res.ID, err
}
return res.ID, nil // useWebhook returns true if we have a webhook defined somewhere
func (b *Bdiscord) useWebhook() bool {
if b.GetString("WebhookURL") != "" {
return true
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
return true
}
}
return false
}
// isWebhookID returns true if the specified id is used in a defined webhook
func (b *Bdiscord) isWebhookID(id string) bool {
if b.GetString("WebhookURL") != "" {
wID, _ := b.splitURL(b.GetString("WebhookURL"))
if wID == id {
return true
}
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
wID, _ := b.splitURL(channel.Options.WebhookURL)
if wID == id {
return true
}
}
}
return false
} }
// handleUploadFile handles native upload of files // handleUploadFile handles native upload of files
@ -338,7 +357,6 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
m := discordgo.MessageSend{ m := discordgo.MessageSend{
Content: msg.Username + fi.Comment, Content: msg.Username + fi.Comment,
Files: []*discordgo.File{&file}, Files: []*discordgo.File{&file},
AllowedMentions: b.getAllowedMentions(),
} }
_, err = b.c.ChannelMessageSendComplex(channelID, &m) _, err = b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil { if err != nil {

View File

@ -2,50 +2,20 @@ 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
} }
// TODO(qaisjp): if other bridges support bulk deletions, it could be fanned out centrally
func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageDeleteBulk) { //nolint:unparam
for _, msgID := range m.Messages {
rmsg := config.Message{
Account: b.Account,
ID: msgID,
Event: config.EventMsgDelete,
Text: config.EventMsgDelete,
Channel: b.getChannelName(m.ChannelID),
}
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
}
func (b *Bdiscord) 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 +24,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)
} }
} }
@ -69,7 +36,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
return return
} }
// if using webhooks, do not relay if it's ours // if using webhooks, do not relay if it's ours
if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) { if b.useWebhook() && m.Author.Bot { // && b.isWebhookID(m.Author.ID) {
return return
} }
@ -84,6 +51,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 +62,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 +79,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,14 +95,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)
// Add our parent id if it exists, and if it's not referring to a message in another channel
if ref := m.MessageReference; ref != nil && ref.ChannelID == m.ChannelID {
rmsg.ParentID = ref.MessageID
}
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
@ -205,33 +168,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,34 +6,10 @@ import (
"strings" "strings"
"unicode" "unicode"
"github.com/matterbridge/discordgo" "github.com/bwmarrin/discordgo"
) )
func (b *Bdiscord) getAllowedMentions() *discordgo.MessageAllowedMentions { func (b *Bdiscord) getNick(user *discordgo.User) string {
// If AllowMention is not specified, then allow all mentions (default Discord behavior)
if !b.IsKeySet("AllowMention") {
return nil
}
// Otherwise, allow only the mentions that are specified
allowedMentionTypes := make([]discordgo.AllowedMentionType, 0, 3)
for _, m := range b.GetStringSlice("AllowMention") {
switch m {
case "everyone":
allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeEveryone)
case "roles":
allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeRoles)
case "users":
allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeUsers)
}
}
return &discordgo.MessageAllowedMentions{
Parse: allowedMentionTypes,
}
}
func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
b.membersMutex.RLock() b.membersMutex.RLock()
defer b.membersMutex.RUnlock() defer b.membersMutex.RUnlock()
@ -47,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)
@ -75,9 +51,6 @@ func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error)
} }
func (b *Bdiscord) getChannelID(name string) string { func (b *Bdiscord) getChannelID(name string) string {
if strings.Contains(name, "/") {
return b.getCategoryChannelID(name)
}
b.channelsMutex.RLock() b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock() defer b.channelsMutex.RUnlock()
@ -86,92 +59,40 @@ func (b *Bdiscord) getChannelID(name string) string {
return idcheck[1] return idcheck[1]
} }
for _, channel := range b.channels { for _, channel := range b.channels {
if channel.Name == name && channel.Type == discordgo.ChannelTypeGuildText { if channel.Name == name {
return channel.ID return channel.ID
} }
} }
return "" return ""
} }
func (b *Bdiscord) getCategoryChannelID(name string) string {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
res := strings.Split(name, "/")
// shouldn't happen because function should be only called from getChannelID
if len(res) != 2 {
return ""
}
catName, chanName := res[0], res[1]
for _, channel := range b.channels {
// if we have a parentID, lookup the name of that parent (category)
// and if it matches return it
if channel.Name == chanName && channel.ParentID != "" {
for _, cat := range b.channels {
if cat.ID == channel.ParentID && cat.Name == catName {
return channel.ID
}
}
}
}
return ""
}
func (b *Bdiscord) getChannelName(id string) string { func (b *Bdiscord) getChannelName(id string) string {
b.channelsMutex.RLock() b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock() defer b.channelsMutex.RUnlock()
for _, 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 channel.Name
} }
} }
return "" return ""
} }
func (b *Bdiscord) getCategoryChannelName(name, parentID string) string {
var usesCat bool
// do we have a category configuration in the channel config
for _, c := range b.channelInfoMap {
if strings.Contains(c.Name, "/") {
usesCat = true
break
}
}
// configuration without category, return the normal channel name
if !usesCat {
return name
}
// create a category/channel response
for _, c := range b.channels {
if c.ID == parentID {
name = c.Name + "/" + name
}
}
return name
}
var ( var (
// See https://discordapp.com/developers/docs/reference#message-formatting. // See https://discordapp.com/developers/docs/reference#message-formatting.
channelMentionRE = regexp.MustCompile("<#[0-9]+>") channelMentionRE = regexp.MustCompile("<#[0-9]+>")
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 {
replaceChannelMentionFunc := func(match string) string { replaceChannelMentionFunc := func(match string) string {
var err error
channelID := match[2 : len(match)-1] channelID := match[2 : len(match)-1]
channelName := b.getChannelName(channelID)
channelName := b.getChannelName(channelID)
// If we don't have the channel refresh our list. // If we don't have the channel refresh our list.
if channelName == "" { if channelName == "" {
var err error
b.channels, err = b.c.GuildChannels(b.guildID) b.channels, err = b.c.GuildChannels(b.guildID)
if err != nil { if err != nil {
return "#unknownchannel" return "#unknownchannel"
@ -207,20 +128,19 @@ 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:], true
return text[1 : length-1], true
} }
return text, false return text, false
} }
// splitURL splits a webhookURL and returns the ID and token. // splitURL splits a webhookURL and returns the ID and token.
func (b *Bdiscord) splitURL(url string) (string, string, bool) { func (b *Bdiscord) splitURL(url string) (string, string) {
const ( const (
expectedWebhookSplitCount = 7 expectedWebhookSplitCount = 7
webhookIdxID = 5 webhookIdxID = 5
@ -228,9 +148,9 @@ func (b *Bdiscord) splitURL(url string) (string, string, bool) {
) )
webhookURLSplit := strings.Split(url, "/") webhookURLSplit := strings.Split(url, "/")
if len(webhookURLSplit) != expectedWebhookSplitCount { if len(webhookURLSplit) != expectedWebhookSplitCount {
return "", "", false b.Log.Fatalf("%s is no correct discord WebhookURL", url)
} }
return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken], true return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken]
} }
func enumerateUsernames(s string) []string { func enumerateUsernames(s string) []string {

View File

@ -1,257 +0,0 @@
// Package transmitter provides functionality for transmitting
// arbitrary webhook messages to Discord.
//
// The package provides the following functionality:
//
// - Creating new webhooks, whenever necessary
// - Loading webhooks that we have previously created
// - Sending new messages
// - Editing messages, via message ID
// - Deleting messages, via message ID
//
// The package has been designed for matterbridge, but with other
// Go bots in mind. The public API should be matterbridge-agnostic.
package transmitter
import (
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/matterbridge/discordgo"
log "github.com/sirupsen/logrus"
)
// A Transmitter represents a message manager for a single guild.
type Transmitter struct {
session *discordgo.Session
guild string
title string
autoCreate bool
// channelWebhooks maps from a channel ID to a webhook instance
channelWebhooks map[string]*discordgo.Webhook
mutex sync.RWMutex
Log *log.Entry
}
// ErrWebhookNotFound is returned when a valid webhook for this channel/message combination does not exist
var ErrWebhookNotFound = errors.New("webhook for this channel and message does not exist")
// ErrPermissionDenied is returned if the bot does not have permission to manage webhooks.
//
// Bots can be granted a guild-wide permission and channel-specific permissions to manage webhooks.
// Despite potentially having guild-wide permission, channel specific overrides could deny a bot's permission to manage webhooks.
var ErrPermissionDenied = errors.New("missing 'Manage Webhooks' permission")
// New returns a new Transmitter given a Discord session, guild ID, and title.
func New(session *discordgo.Session, guild string, title string, autoCreate bool) *Transmitter {
return &Transmitter{
session: session,
guild: guild,
title: title,
autoCreate: autoCreate,
channelWebhooks: make(map[string]*discordgo.Webhook),
Log: log.NewEntry(log.StandardLogger()),
}
}
// Send transmits a message to the given channel with the provided webhook data, and waits until Discord responds with message data.
func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) {
wh, err := t.getOrCreateWebhook(channelID)
if err != nil {
return nil, err
}
msg, err := t.session.WebhookExecute(wh.ID, wh.Token, true, params)
if err != nil {
return nil, fmt.Errorf("execute failed: %w", err)
}
return msg, nil
}
// Edit will edit a message in a channel, if possible.
func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo.WebhookParams) error {
wh := t.getWebhook(channelID)
if wh == nil {
return ErrWebhookNotFound
}
uri := discordgo.EndpointWebhookToken(wh.ID, wh.Token) + "/messages/" + messageID
_, err := t.session.RequestWithBucketID("PATCH", uri, params, discordgo.EndpointWebhookToken("", ""))
if err != nil {
return err
}
return nil
}
// HasWebhook checks whether the transmitter is using a particular webhook.
func (t *Transmitter) HasWebhook(id string) bool {
t.mutex.RLock()
defer t.mutex.RUnlock()
for _, wh := range t.channelWebhooks {
if wh.ID == id {
return true
}
}
return false
}
// AddWebhook allows you to register a channel's webhook with the transmitter.
func (t *Transmitter) AddWebhook(channelID string, webhook *discordgo.Webhook) bool {
t.Log.Debugf("Manually added webhook %#v to channel %#v", webhook.ID, channelID)
t.mutex.Lock()
defer t.mutex.Unlock()
_, replaced := t.channelWebhooks[channelID]
t.channelWebhooks[channelID] = webhook
return replaced
}
// RefreshGuildWebhooks loads "relevant" webhooks into the transmitter, with careful permission handling.
//
// Notes:
//
// - A webhook is "relevant" if it was created by this bot -- the ApplicationID should match the bot's ID.
// - The term "having permission" means having the "Manage Webhooks" permission. See ErrPermissionDenied for more information.
// - This function is additive and will not unload previously loaded webhooks.
// - A nil channelIDs slice is treated the same as an empty one.
//
// If the bot has guild-wide permission:
//
// 1. it will load any "relevant" webhooks from the entire guild
// 2. the given slice is ignored
//
// If the bot does not have guild-wide permission:
//
// 1. it will load any "relevant" webhooks in each channel
// 2. a single error will be returned if any error occurs (incl. if there is no permission for any of these channels)
//
// If any channel has more than one "relevant" webhook, it will randomly pick one.
func (t *Transmitter) RefreshGuildWebhooks(channelIDs []string) error {
t.Log.Debugln("Refreshing guild webhooks")
botID, err := getDiscordUserID(t.session)
if err != nil {
return fmt.Errorf("could not get current user: %w", err)
}
// Get all existing webhooks
hooks, err := t.session.GuildWebhooks(t.guild)
if err != nil {
switch {
case isDiscordPermissionError(err):
// We fallback on manually fetching hooks from individual channels
// if we don't have the "Manage Webhooks" permission globally.
// We can only do this if we were provided channelIDs, though.
if len(channelIDs) == 0 {
return ErrPermissionDenied
}
t.Log.Debugln("Missing global 'Manage Webhooks' permission, falling back on per-channel permission")
return t.fetchChannelsHooks(channelIDs, botID)
default:
return fmt.Errorf("could not get webhooks: %w", err)
}
}
t.Log.Debugln("Refreshing guild webhooks using global permission")
t.assignHooksByAppID(hooks, botID, false)
return nil
}
// createWebhook creates a webhook for a specific channel.
func (t *Transmitter) createWebhook(channel string) (*discordgo.Webhook, error) {
t.mutex.Lock()
defer t.mutex.Unlock()
wh, err := t.session.WebhookCreate(channel, t.title+time.Now().Format(" 3:04:05PM"), "")
if err != nil {
return nil, err
}
t.channelWebhooks[channel] = wh
return wh, nil
}
func (t *Transmitter) getWebhook(channel string) *discordgo.Webhook {
t.mutex.RLock()
defer t.mutex.RUnlock()
return t.channelWebhooks[channel]
}
func (t *Transmitter) getOrCreateWebhook(channelID string) (*discordgo.Webhook, error) {
// If we have a webhook for this channel, immediately return it
wh := t.getWebhook(channelID)
if wh != nil {
return wh, nil
}
// Early exit if we don't want to automatically create one
if !t.autoCreate {
return nil, ErrWebhookNotFound
}
t.Log.Infof("Creating a webhook for %s\n", channelID)
wh, err := t.createWebhook(channelID)
if err != nil {
return nil, fmt.Errorf("could not create webhook: %w", err)
}
return wh, nil
}
// fetchChannelsHooks fetches hooks for the given channelIDs and calls assignHooksByAppID for each channel's hooks
func (t *Transmitter) fetchChannelsHooks(channelIDs []string, botID string) error {
// For each channel, search for relevant hooks
var failedHooks []string
for _, channelID := range channelIDs {
hooks, err := t.session.ChannelWebhooks(channelID)
if err != nil {
failedHooks = append(failedHooks, "\n- "+channelID+": "+err.Error())
continue
}
t.assignHooksByAppID(hooks, botID, true)
}
// Compose an error if any hooks failed
if len(failedHooks) > 0 {
return errors.New("failed to fetch hooks:" + strings.Join(failedHooks, ""))
}
return nil
}
func (t *Transmitter) assignHooksByAppID(hooks []*discordgo.Webhook, appID string, channelTargeted bool) {
logLine := "Picking up webhook"
if channelTargeted {
logLine += " (channel targeted)"
}
t.mutex.Lock()
defer t.mutex.Unlock()
for _, wh := range hooks {
if wh.ApplicationID != appID {
continue
}
t.channelWebhooks[wh.ChannelID] = wh
t.Log.WithFields(log.Fields{
"id": wh.ID,
"name": wh.Name,
"channel": wh.ChannelID,
}).Println(logLine)
}
}

View File

@ -1,32 +0,0 @@
package transmitter
import (
"github.com/matterbridge/discordgo"
)
// isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions
func isDiscordPermissionError(err error) bool {
if err == nil {
return false
}
restErr, ok := err.(*discordgo.RESTError)
if !ok {
return false
}
return restErr.Message != nil && restErr.Message.Code == discordgo.ErrCodeMissingPermissions
}
// getDiscordUserID gets own user ID from state, and fallback on API request
func getDiscordUserID(session *discordgo.Session) (string, error) {
if user := session.State.User; user != nil {
return user.ID, nil
}
user, err := session.User("@me")
if err != nil {
return "", err
}
return user.ID, nil
}

View File

@ -1,148 +0,0 @@
package bdiscord
import (
"bytes"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/discordgo"
)
// shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks
func (b *Bdiscord) shouldMessageUseWebhooks(msg *config.Message) bool {
if b.useAutoWebhooks {
return true
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
if ci.Options.WebhookURL != "" {
return true
}
}
return false
}
// maybeGetLocalAvatar checks if UseLocalAvatar contains the message's
// account or protocol, and if so, returns the Discord avatar (if exists)
func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
for _, val := range b.GetStringSlice("UseLocalAvatar") {
if msg.Protocol != val && msg.Account != val {
continue
}
member, err := b.getGuildMemberByNick(msg.Username)
if err != nil {
return ""
}
return member.User.AvatarURL("")
}
return ""
}
// webhookSend send one or more message via webhook, taking care of file
// uploads (from slack, telegram or mattermost).
// Returns messageID and error.
func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) {
var (
res *discordgo.Message
err error
)
// If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this)
if msg.Avatar == "" {
msg.Avatar = b.maybeGetLocalAvatar(msg)
}
// WebhookParams can have either `Content` or `File`.
// We can't send empty messages.
if msg.Text != "" {
res, err = b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
AllowedMentions: b.getAllowedMentions(),
},
)
if err != nil {
b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
}
}
if msg.Extra != nil {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := discordgo.File{
Name: fi.Name,
ContentType: "",
Reader: bytes.NewReader(*fi.Data),
}
content := fi.Comment
_, e2 := b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Username: msg.Username,
AvatarURL: msg.Avatar,
File: &file,
Content: content,
AllowedMentions: b.getAllowedMentions(),
},
)
if e2 != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
}
}
}
return res, err
}
func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (string, error) {
// skip events
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
return "", nil
}
// skip empty messages
if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) {
b.Log.Debugf("Skipping empty message %#v", msg)
return "", nil
}
msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
msg.Text = b.replaceUserMentions(msg.Text)
// discord username must be [0..32] max
if len(msg.Username) > 32 {
msg.Username = msg.Username[0:32]
}
if msg.ID != "" {
b.Log.Debugf("Editing webhook message")
err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AllowedMentions: b.getAllowedMentions(),
})
if err == nil {
return msg.ID, nil
}
b.Log.Errorf("Could not edit webhook message: %s", err)
}
b.Log.Debugf("Processing webhook sending for message %#v", msg)
discordMsg, err := b.webhookSend(msg, channelID)
if err != nil {
b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msg, err)
return "", err
}
if discordMsg == nil {
return "", nil
}
return discordMsg.ID, nil
}

View File

@ -1,252 +0,0 @@
package harmony
import (
"fmt"
"log"
"strconv"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/harmony-development/shibshib"
chatv1 "github.com/harmony-development/shibshib/gen/chat/v1"
typesv1 "github.com/harmony-development/shibshib/gen/harmonytypes/v1"
profilev1 "github.com/harmony-development/shibshib/gen/profile/v1"
)
type cachedProfile struct {
data *profilev1.GetProfileResponse
lastUpdated time.Time
}
type Bharmony struct {
*bridge.Config
c *shibshib.Client
profileCache map[uint64]cachedProfile
}
func uToStr(in uint64) string {
return strconv.FormatUint(in, 10)
}
func strToU(in string) (uint64, error) {
return strconv.ParseUint(in, 10, 64)
}
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bharmony{
Config: cfg,
profileCache: map[uint64]cachedProfile{},
}
return b
}
func (b *Bharmony) getProfile(u uint64) (*profilev1.GetProfileResponse, error) {
if v, ok := b.profileCache[u]; ok && time.Since(v.lastUpdated) < time.Minute*10 {
return v.data, nil
}
resp, err := b.c.ProfileKit.GetProfile(&profilev1.GetProfileRequest{
UserId: u,
})
if err != nil {
if v, ok := b.profileCache[u]; ok {
return v.data, nil
}
return nil, err
}
b.profileCache[u] = cachedProfile{
data: resp,
lastUpdated: time.Now(),
}
return resp, nil
}
func (b *Bharmony) avatarFor(m *chatv1.Message) string {
if m.Overrides != nil {
return m.Overrides.GetAvatar()
}
profi, err := b.getProfile(m.AuthorId)
if err != nil {
return ""
}
return b.c.TransformHMCURL(profi.Profile.GetUserAvatar())
}
func (b *Bharmony) usernameFor(m *chatv1.Message) string {
if m.Overrides != nil {
return m.Overrides.GetUsername()
}
profi, err := b.getProfile(m.AuthorId)
if err != nil {
return ""
}
return profi.Profile.UserName
}
func (b *Bharmony) toMessage(msg *shibshib.LocatedMessage) config.Message {
message := config.Message{}
message.Account = b.Account
message.UserID = uToStr(msg.Message.AuthorId)
message.Avatar = b.avatarFor(msg.Message)
message.Username = b.usernameFor(msg.Message)
message.Channel = uToStr(msg.ChannelID)
message.ID = uToStr(msg.MessageId)
switch content := msg.Message.Content.Content.(type) {
case *chatv1.Content_EmbedMessage:
message.Text = "Embed"
case *chatv1.Content_AttachmentMessage:
var s strings.Builder
for idx, attach := range content.AttachmentMessage.Files {
s.WriteString(b.c.TransformHMCURL(attach.Id))
if idx < len(content.AttachmentMessage.Files)-1 {
s.WriteString(", ")
}
}
message.Text = s.String()
case *chatv1.Content_PhotoMessage:
var s strings.Builder
for idx, attach := range content.PhotoMessage.GetPhotos() {
s.WriteString(attach.GetCaption().GetText())
s.WriteString("\n")
s.WriteString(b.c.TransformHMCURL(attach.GetHmc()))
if idx < len(content.PhotoMessage.GetPhotos())-1 {
s.WriteString("\n\n")
}
}
message.Text = s.String()
case *chatv1.Content_TextMessage:
message.Text = content.TextMessage.Content.Text
}
return message
}
func (b *Bharmony) outputMessages() {
for {
msg := <-b.c.EventsStream()
if msg.Message.AuthorId == b.c.UserID {
continue
}
b.Remote <- b.toMessage(msg)
}
}
func (b *Bharmony) GetUint64(conf string) uint64 {
num, err := strToU(b.GetString(conf))
if err != nil {
log.Fatal(err)
}
return num
}
func (b *Bharmony) Connect() (err error) {
b.c, err = shibshib.NewClient(b.GetString("Homeserver"), b.GetString("Token"), b.GetUint64("UserID"))
if err != nil {
return
}
b.c.SubscribeToGuild(b.GetUint64("Community"))
go b.outputMessages()
return nil
}
func (b *Bharmony) send(msg config.Message) (id string, err error) {
msgChan, err := strToU(msg.Channel)
if err != nil {
return
}
retID, err := b.c.ChatKit.SendMessage(&chatv1.SendMessageRequest{
GuildId: b.GetUint64("Community"),
ChannelId: msgChan,
Content: &chatv1.Content{
Content: &chatv1.Content_TextMessage{
TextMessage: &chatv1.Content_TextContent{
Content: &chatv1.FormattedText{
Text: msg.Text,
},
},
},
},
Overrides: &chatv1.Overrides{
Username: &msg.Username,
Avatar: &msg.Avatar,
Reason: &chatv1.Overrides_Bridge{Bridge: &typesv1.Empty{}},
},
InReplyTo: nil,
EchoId: nil,
Metadata: nil,
})
if err != nil {
err = fmt.Errorf("send: error sending message: %w", err)
log.Println(err.Error())
}
return uToStr(retID.MessageId), err
}
func (b *Bharmony) delete(msg config.Message) (id string, err error) {
msgChan, err := strToU(msg.Channel)
if err != nil {
return "", err
}
msgID, err := strToU(msg.ID)
if err != nil {
return "", err
}
_, err = b.c.ChatKit.DeleteMessage(&chatv1.DeleteMessageRequest{
GuildId: b.GetUint64("Community"),
ChannelId: msgChan,
MessageId: msgID,
})
return "", err
}
func (b *Bharmony) typing(msg config.Message) (id string, err error) {
msgChan, err := strToU(msg.Channel)
if err != nil {
return "", err
}
_, err = b.c.ChatKit.Typing(&chatv1.TypingRequest{
GuildId: b.GetUint64("Community"),
ChannelId: msgChan,
})
return "", err
}
func (b *Bharmony) Send(msg config.Message) (id string, err error) {
switch msg.Event {
case "":
return b.send(msg)
case config.EventMsgDelete:
return b.delete(msg)
case config.EventUserTyping:
return b.typing(msg)
default:
return "", nil
}
}
func (b *Bharmony) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Bharmony) Disconnect() error {
return nil
}

View File

@ -14,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.
@ -48,30 +46,6 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) {
return &data, nil return &data, nil
} }
// DownloadFileAuthRocket downloads the given URL using the specified Rocket user ID and authentication token.
func DownloadFileAuthRocket(url, token, userID string) (*[]byte, error) {
var buf bytes.Buffer
client := &http.Client{
Timeout: time.Second * 5,
}
req, err := http.NewRequest("GET", url, nil)
req.Header.Add("X-Auth-Token", token)
req.Header.Add("X-User-Id", userID)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
_, err = io.Copy(&buf, resp.Body)
data := buf.Bytes()
return &data, err
}
// GetSubLines splits messages in newline-delimited lines. If maxLineLength is // GetSubLines splits messages in newline-delimited lines. If maxLineLength is
// specified as non-zero GetSubLines will also clip long lines to the maximum // specified as non-zero GetSubLines will also clip long lines to the maximum
// length and insert a warning marker that the line was clipped. // length and insert a warning marker that the line was clipped.
@ -79,10 +53,8 @@ func DownloadFileAuthRocket(url, token, userID string) (*[]byte, error) {
// TODO: The current implementation has the inconvenient that it disregards // TODO: The current implementation has the inconvenient that it disregards
// word boundaries when splitting but this is hard to solve without potentially // word boundaries when splitting but this is hard to solve without potentially
// breaking formatting and other stylistic effects. // breaking formatting and other stylistic effects.
func GetSubLines(message string, maxLineLength int, clippingMessage string) []string { func GetSubLines(message string, maxLineLength int) []string {
if clippingMessage == "" { const clippingMessage = " <clipped message>"
clippingMessage = " <clipped message>"
}
var lines []string var lines []string
for _, line := range strings.Split(strings.TrimSpace(message), "\n") { for _, line := range strings.Split(strings.TrimSpace(message), "\n") {
@ -192,11 +164,8 @@ func RemoveEmptyNewLines(msg string) string {
// ClipMessage trims a message to the specified length if it exceeds it and adds a warning // ClipMessage trims a message to the specified length if it exceeds it and adds a warning
// to the message in case it does so. // to the message in case it does so.
func ClipMessage(text string, length int, clippingMessage string) string { func ClipMessage(text string, length int) string {
if clippingMessage == "" { const clippingMessage = " <clipped message>"
clippingMessage = " <clipped message>"
}
if len(text) > length { if len(text) > length {
text = text[:length-len(clippingMessage)] text = text[:length-len(clippingMessage)]
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError { if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
@ -207,21 +176,15 @@ func ClipMessage(text string, length int, clippingMessage string) 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 | parser.FencedCode 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)

View File

@ -10,7 +10,8 @@ import (
const testLineLength = 64 const testLineLength = 64
var lineSplittingTestCases = map[string]struct { var (
lineSplittingTestCases = map[string]struct {
input string input string
splitOutput []string splitOutput []string
nonSplitOutput []string nonSplitOutput []string
@ -89,17 +90,18 @@ var lineSplittingTestCases = map[string]struct {
nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"}, nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
}, },
} }
)
func TestGetSubLines(t *testing.T) { func TestGetSubLines(t *testing.T) {
for testname, testcase := range lineSplittingTestCases { for testname, testcase := range lineSplittingTestCases {
splitLines := GetSubLines(testcase.input, testLineLength, "") splitLines := GetSubLines(testcase.input, testLineLength)
assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname) assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname)
for _, splitLine := range splitLines { for _, splitLine := range splitLines {
byteLength := len([]byte(splitLine)) byteLength := len([]byte(splitLine))
assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength) assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength)
} }
nonSplitLines := GetSubLines(testcase.input, 0, "") nonSplitLines := GetSubLines(testcase.input, 0)
assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname) assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname)
} }
} }
@ -108,19 +110,16 @@ func TestConvertWebPToPNG(t *testing.T) {
if os.Getenv("LOCAL_TEST") == "" { if os.Getenv("LOCAL_TEST") == "" {
t.Skip() t.Skip()
} }
input, err := ioutil.ReadFile("test.webp") input, err := ioutil.ReadFile("test.webp")
if err != nil { if err != nil {
t.Fail() t.Fail()
} }
d := &input d := &input
err = ConvertWebPToPNG(d) err = ConvertWebPToPNG(d)
if err != nil { if err != nil {
t.Fail() t.Fail()
} }
err = ioutil.WriteFile("test.png", *d, 0644)
err = ioutil.WriteFile("test.png", *d, 0o644) // nolint:gosec
if err != nil { if err != nil {
t.Fail() t.Fail()
} }

View File

@ -1,36 +0,0 @@
//go:build cgo
// +build cgo
package helper
import (
"fmt"
"github.com/Benau/tgsconverter/libtgsconverter"
"github.com/sirupsen/logrus"
)
func CanConvertTgsToX() error {
return nil
}
// ConvertTgsToX convert input data (which should be tgs format) to any format supported by libtgsconverter
func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
options := libtgsconverter.NewConverterOptions()
options.SetExtension(outputFormat)
blob, err := libtgsconverter.ImportFromData(*data, options)
if err != nil {
return fmt.Errorf("failed to run libtgsconverter.ImportFromData: %s", err.Error())
}
*data = blob
return nil
}
func SupportsFormat(format string) bool {
return libtgsconverter.SupportsExtension(format)
}
func LottieBackend() string {
return "libtgsconverter"
}

View File

@ -1,89 +0,0 @@
// +build !cgo
package helper
import (
"io/ioutil"
"os"
"os/exec"
"github.com/sirupsen/logrus"
)
// 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:
tmpInFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-input-*.tgs")
if err != nil {
return err
}
tmpInFileName := tmpInFile.Name()
defer func() {
if removeErr := os.Remove(tmpInFileName); removeErr != nil {
logger.Errorf("Could not delete temporary (input) file %s: %v", tmpInFileName, removeErr)
}
}()
// lottie can handle writing to a pipe, but there is no way to do that platform-independently.
// "/dev/stdout" won't work on Windows, and "-" upsets Cairo for some reason. So we need another file:
tmpOutFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-output-*.data")
if err != nil {
return err
}
tmpOutFileName := tmpOutFile.Name()
defer func() {
if removeErr := os.Remove(tmpOutFileName); removeErr != nil {
logger.Errorf("Could not delete temporary (output) file %s: %v", tmpOutFileName, removeErr)
}
}()
if _, writeErr := tmpInFile.Write(*data); writeErr != nil {
return writeErr
}
// Must close before calling lottie to avoid data races:
if closeErr := tmpInFile.Close(); closeErr != nil {
return closeErr
}
// Call lottie to transform:
cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpInFileName, tmpOutFileName)
cmd.Stdout = nil
cmd.Stderr = nil
// NB: lottie writes progress into to stderr in all cases.
_, stderr := cmd.Output()
if stderr != nil {
// 'stderr' already contains some parts of Stderr, because it was set to 'nil'.
return stderr
}
dataContents, err := ioutil.ReadFile(tmpOutFileName)
if err != nil {
return err
}
*data = dataContents
return nil
}
func SupportsFormat(format string) bool {
switch format {
case "png":
fallthrough
case "webp":
return true
default:
return false
}
return false
}
func LottieBackend() string {
return "lottie_convert.py"
}

View File

@ -4,14 +4,15 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"regexp"
"strconv" "strconv"
"strings" "strings"
"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/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"
@ -67,20 +68,6 @@ func (b *Birc) handleFiles(msg *config.Message) bool {
return true return true
} }
func (b *Birc) handleInvite(client *girc.Client, event girc.Event) {
if len(event.Params) != 2 {
return
}
channel := event.Params[1]
b.Log.Debugf("got invite for %s", channel)
if _, ok := b.channels[channel]; ok {
b.i.Cmd.Join(channel)
}
}
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) { func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
if len(event.Params) == 0 { if len(event.Params) == 0 {
b.Log.Debugf("handleJoinPart: empty Params? %#v", event) b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
@ -104,13 +91,8 @@ func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
if b.GetBool("nosendjoinpart") { if b.GetBool("nosendjoinpart") {
return return
} }
msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
if b.GetBool("verbosejoinpart") {
b.Log.Debugf("<= Sending verbose JOIN_LEAVE event from %s to gateway", b.Account)
msg = config.Message{Username: "system", Text: event.Source.Name + " (" + event.Source.Ident + "@" + event.Source.Host + ") " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
} else {
b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account) b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account)
} msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
b.Log.Debugf("<= Message is %#v", msg) b.Log.Debugf("<= Message is %#v", msg)
b.Remote <- msg b.Remote <- msg
return return
@ -123,15 +105,14 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
i := b.i i := b.i
b.Nick = event.Params[0] b.Nick = event.Params[0]
i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg) i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg) i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg)
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.Handlers.AddBg(girc.NOTICE, b.handleNotice) i.Handlers.Add(girc.NOTICE, b.handleNotice)
i.Handlers.AddBg("JOIN", b.handleJoinPart) i.Handlers.Add("JOIN", b.handleJoinPart)
i.Handlers.AddBg("PART", b.handleJoinPart) i.Handlers.Add("PART", b.handleJoinPart)
i.Handlers.AddBg("QUIT", b.handleJoinPart) i.Handlers.Add("QUIT", b.handleJoinPart)
i.Handlers.AddBg("KICK", b.handleJoinPart) i.Handlers.Add("KICK", b.handleJoinPart)
i.Handlers.Add("INVITE", b.handleInvite)
} }
func (b *Birc) handleNickServ() { func (b *Birc) handleNickServ() {
@ -185,14 +166,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
if b.skipPrivMsg(event) { if b.skipPrivMsg(event) {
return return
} }
rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
rmsg := config.Message{
Username: event.Source.Name,
Channel: strings.ToLower(event.Params[0]),
Account: b.Account,
UserID: event.Source.Ident + "@" + event.Source.Host,
}
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event) b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
// set action event // set action event
@ -200,14 +174,13 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
rmsg.Event = config.EventUserAction rmsg.Event = config.EventUserAction
} }
// set NOTICE event
if event.Command == "NOTICE" {
rmsg.Event = config.EventNoticeIRC
}
// strip action, we made an event if it was an action // strip action, we made an event if it was an action
rmsg.Text += event.StripAction() rmsg.Text += event.StripAction()
// strip IRC colors
re := regexp.MustCompile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
rmsg.Text = re.ReplaceAllString(rmsg.Text, "")
// start detecting the charset // start detecting the charset
mycharset := b.GetString("Charset") mycharset := b.GetString("Charset")
if mycharset == "" { if mycharset == "" {

View File

@ -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
@ -30,7 +28,6 @@ type Birc struct {
Local chan config.Message // local queue for flood control Local chan config.Message // local queue for flood control
FirstConnection, authDone bool FirstConnection, authDone bool
MessageDelay, MessageQueue, MessageLength int MessageDelay, MessageQueue, MessageLength int
channels map[string]bool
*bridge.Config *bridge.Config
} }
@ -41,8 +38,6 @@ func New(cfg *bridge.Config) bridge.Bridger {
b.Nick = b.GetString("Nick") b.Nick = b.GetString("Nick")
b.names = make(map[string][]string) b.names = make(map[string][]string)
b.connected = make(chan error) b.connected = make(chan error)
b.channels = make(map[string]bool)
if b.GetInt("MessageDelay") == 0 { if b.GetInt("MessageDelay") == 0 {
b.MessageDelay = 1300 b.MessageDelay = 1300
} else { } else {
@ -115,7 +110,6 @@ func (b *Birc) Disconnect() error {
} }
func (b *Birc) JoinChannel(channel config.ChannelInfo) error { func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
b.channels[channel.Name] = true
// need to check if we have nickserv auth done before joining channels // need to check if we have nickserv auth done before joining channels
for { for {
if b.authDone { if b.authDone {
@ -162,14 +156,10 @@ 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, b.GetString("MessageClipped")) msgLines = helper.GetSubLines(msg.Text, b.MessageLength)
} else { } else {
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped")) msgLines = helper.GetSubLines(msg.Text, 0)
} }
for i := range msgLines { for i := range msgLines {
if len(b.Local) >= b.MessageQueue { if len(b.Local) >= b.MessageQueue {
@ -177,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
} }
@ -205,61 +199,25 @@ func (b *Birc) doConnect() {
} }
} }
// Sanitize nicks for RELAYMSG: replace IRC characters with special meanings with "-"
func sanitizeNick(nick string) string {
sanitize := func(r rune) rune {
if strings.ContainsRune("!+%@&#$:'\"?*,. ", r) {
return '-'
}
return r
}
return strings.Map(sanitize, nick)
}
func (b *Birc) doSend() { func (b *Birc) doSend() {
rate := time.Millisecond * time.Duration(b.MessageDelay) rate := time.Millisecond * time.Duration(b.MessageDelay)
throttle := time.NewTicker(rate) throttle := time.NewTicker(rate)
for msg := range b.Local { for msg := range b.Local {
<-throttle.C <-throttle.C
username := msg.Username username := msg.Username
// Optional support for the proposed RELAYMSG extension, described at
// https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md
// nolint:nestif
if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) &&
b.GetBool("UseRelayMsg") {
username = sanitizeNick(username)
text := msg.Text
// Work around girc chomping leading commas on single word messages?
if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') {
text = ":" + text
}
if msg.Event == config.EventUserAction {
b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck
} else {
b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username)
b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck
}
} else {
if b.GetBool("Colornicks") { 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)
} }
switch msg.Event { if msg.Event == config.EventUserAction {
case config.EventUserAction:
b.i.Cmd.Action(msg.Channel, username+msg.Text) b.i.Cmd.Action(msg.Channel, username+msg.Text)
case config.EventNoticeIRC: } else {
b.Log.Debugf("Sending notice to channel %s", msg.Channel)
b.i.Cmd.Notice(msg.Channel, username+msg.Text)
default:
b.Log.Debugf("Sending to channel %s", msg.Channel) b.Log.Debugf("Sending to channel %s", msg.Channel)
b.i.Cmd.Message(msg.Channel, username+msg.Text) b.i.Cmd.Message(msg.Channel, username+msg.Text)
} }
} }
} }
}
// validateInput validates the server/port/nick configuration. Returns a *girc.Client if successful // validateInput validates the server/port/nick configuration. Returns a *girc.Client if successful
func (b *Birc) getClient() (*girc.Client, error) { func (b *Birc) getClient() (*girc.Client, error) {
@ -271,11 +229,8 @@ func (b *Birc) getClient() (*girc.Client, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
user := b.GetString("UserName")
if user == "" {
user = b.GetString("Nick")
}
// fix strict user handling of girc // fix strict user handling of girc
user := b.GetString("Nick")
for !girc.IsValidUser(user) { for !girc.IsValidUser(user) {
if len(user) == 1 || len(user) == 0 { if len(user) == 1 || len(user) == 0 {
user = "matterbridge" user = "matterbridge"
@ -283,22 +238,6 @@ func (b *Birc) getClient() (*girc.Client, error) {
} }
user = user[1:] user = user[1:]
} }
realName := b.GetString("RealName")
if realName == "" {
realName = b.GetString("Nick")
}
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,
@ -306,15 +245,10 @@ func (b *Birc) getClient() (*girc.Client, error) {
Port: port, Port: port,
Nick: b.GetString("Nick"), Nick: b.GetString("Nick"),
User: user, User: user,
Name: realName, Name: b.GetString("Nick"),
SSL: b.GetBool("UseTLS"), SSL: b.GetBool("UseTLS"),
Bind: b.GetString("Bind"),
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,
SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil},
}) })
return i, nil return i, nil
} }
@ -324,16 +258,12 @@ func (b *Birc) endNames(client *girc.Client, event girc.Event) {
sort.Strings(b.names[channel]) sort.Strings(b.names[channel])
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow() maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
for len(b.names[channel]) > maxNamesPerPost { for len(b.names[channel]) > maxNamesPerPost {
b.Remote <- config.Message{ b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]),
Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]), Channel: channel, Account: b.Account}
Channel: channel, Account: b.Account,
}
b.names[channel] = b.names[channel][maxNamesPerPost:] b.names[channel] = b.names[channel][maxNamesPerPost:]
} }
b.Remote <- config.Message{ b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel]),
Username: b.Nick, Text: b.formatnicks(b.names[channel]), Channel: channel, Account: b.Account}
Channel: channel, Account: b.Account,
}
b.names[channel] = nil b.names[channel] = nil
b.i.Handlers.Clear(girc.RPL_NAMREPLY) b.i.Handlers.Clear(girc.RPL_NAMREPLY)
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES) b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
@ -344,7 +274,7 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
b.Nick = b.i.GetNick() b.Nick = b.i.GetNick()
// freenode doesn't send 001 as first reply // freenode doesn't send 001 as first reply
if event.Command == "NOTICE" && len(event.Params) != 2 { if event.Command == "NOTICE" {
return true return true
} }
// don't forward queries to the bot // don't forward queries to the bot
@ -355,15 +285,6 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
if event.Source.Name == b.Nick { if event.Source.Name == b.Nick {
return true return true
} }
// don't forward messages we sent via RELAYMSG
if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick {
return true
}
// This is the old name of the cap sent in spoofed messages; I've kept this in
// for compatibility reasons
if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick {
return true
}
return false return false
} }

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,215 +0,0 @@
package bmatrix
import (
"encoding/json"
"errors"
"fmt"
"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.
// In addition, we also detect if another user have the same username, and if so, we append their mxids to their usernames to differentiate them.
toDelete := []string{}
conflict := false
b.Lock()
for mxid, v := range b.NicknameMap {
// to prevent username reuse across matrix servers - or even on the same server, append
// the mxid to the username when there is a conflict
if v.displayName == displayName {
conflict = true
// TODO: it would be nice to be able to rename previous messages from this user.
// The current behavior is that only users with clashing usernames and *that have spoken since the bridge last started* will get their mxids shown, and I don't know if that's the expected behavior.
v.displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
b.NicknameMap[mxid] = v
}
if now.Sub(v.lastUpdated) > 10*time.Minute {
toDelete = append(toDelete, mxid)
}
}
if conflict {
displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
}
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
}
// handleRatelimit handles the ratelimit errors and return if we're ratelimited and the amount of time to sleep
func (b *Bmatrix) handleRatelimit(err error) (time.Duration, bool) {
httpErr := handleError(err)
if httpErr.Errcode != "M_LIMIT_EXCEEDED" {
return 0, false
}
b.Log.Debugf("ratelimited: %s", httpErr.Err)
b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before retrying", httpErr.RetryAfterMs/1000)
return time.Duration(httpErr.RetryAfterMs) * time.Millisecond, true
}
// retry function will check if we're ratelimited and retries again when backoff time expired
// returns original error if not 429 ratelimit
func (b *Bmatrix) retry(f func() error) error {
b.rateMutex.Lock()
defer b.rateMutex.Unlock()
for {
if err := f(); err != nil {
if backoff, ok := b.handleRatelimit(err); ok {
time.Sleep(backoff)
} else {
return err
}
} else {
return nil
}
}
}

View File

@ -3,101 +3,37 @@ 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
rateMutex sync.RWMutex
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
}
type InReplyToRelationContent struct {
EventID string `json:"event_id"`
}
type InReplyToRelation struct {
InReplyTo InReplyToRelationContent `json:"m.in_reply_to"`
}
type ReplyMessage struct {
RelatedTo InReplyToRelation `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
} }
func (b *Bmatrix) Connect() error { func (b *Bmatrix) Connect() error {
var err error var err error
b.Log.Infof("Connecting %s", b.GetString("Server")) b.Log.Infof("Connecting %s", b.GetString("Server"))
if b.GetString("MxID") != "" && b.GetString("Token") != "" {
b.mc, err = matrix.NewClient(
b.GetString("Server"), b.GetString("MxID"), b.GetString("Token"),
)
if err != nil {
return err
}
b.UserID = b.GetString("MxID")
b.Log.Info("Using existing Matrix credentials")
} else {
b.mc, err = matrix.NewClient(b.GetString("Server"), "", "") b.mc, err = matrix.NewClient(b.GetString("Server"), "", "")
if err != nil { if err != nil {
return err return err
@ -106,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
@ -114,7 +49,6 @@ func (b *Bmatrix) Connect() error {
b.mc.SetCredentials(resp.UserID, resp.AccessToken) b.mc.SetCredentials(resp.UserID, resp.AccessToken)
b.UserID = resp.UserID b.UserID = resp.UserID
b.Log.Info("Connection succeeded") b.Log.Info("Connection succeeded")
}
go b.handlematrix() go b.handlematrix()
return nil return nil
} }
@ -124,18 +58,14 @@ func (b *Bmatrix) Disconnect() error {
} }
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error { func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
return b.retry(func() error {
resp, err := b.mc.JoinRoom(channel.Name, "", nil) resp, err := b.mc.JoinRoom(channel.Name, "", nil)
if err != nil { if err != nil {
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) {
@ -144,30 +74,17 @@ 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,
} }
msgID := ""
err := b.retry(func() error {
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 {
return err return "", err
} }
return resp.EventID, err
msgID = resp.EventID
return err
})
return msgID, err
} }
// Delete message // Delete message
@ -175,34 +92,17 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
if msg.ID == "" { if msg.ID == "" {
return "", nil return "", nil
} }
msgID := ""
err := b.retry(func() error {
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{}) resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
if err != nil { if err != nil {
return err return "", err
} }
return resp.EventID, err
msgID = resp.EventID
return err
})
return msgID, err
} }
// 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) {
rmsg := rmsg if _, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text); err != nil {
err := b.retry(func() error {
_, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text)
return err
})
if err != nil {
b.Log.Errorf("sendText failed: %s", err) b.Log.Errorf("sendText failed: %s", err)
} }
} }
@ -213,142 +113,47 @@ 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.retry(func() error {
_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
return err
})
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,
}
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
return err
})
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)
var ( // check if we have a </tag>. if we have, we don't escape HTML. #696
resp *matrix.RespSendEvent if b.htmlTag.MatchString(msg.Username) {
err error username = msg.Username
)
err = b.retry(func() error {
resp, err = b.mc.SendText(channel, username.plain+msg.Text)
return err
})
if err != nil {
return "", err
} }
return resp.EventID, err
}
if msg.ParentValid() {
m := ReplyMessage{
TextMessage: matrix.TextMessage{
MsgType: "m.text",
Body: username.plain + msg.Text,
FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text),
},
}
m.RelatedTo = InReplyToRelation{
InReplyTo: InReplyToRelationContent{
EventID: msg.ParentID,
},
}
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
return err
})
if err != nil {
return "", err
}
return resp.EventID, err
}
// Post normal message with HTML support (eg riot.im) // Post normal message with HTML support (eg riot.im)
var ( resp, err := b.mc.SendHTML(channel, msg.Username+msg.Text, username+helper.ParseMarkdown(msg.Text))
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendFormattedText(channel, username.plain+msg.Text,
username.formatted+helper.ParseMarkdown(msg.Text))
return err
})
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 b == nil {
return
}
if err := b.mc.Sync(); err != nil { if err := b.mc.Sync(); err != nil {
b.Log.Println("Sync() returned ", err) b.Log.Println("Sync() returned ", err)
} }
@ -356,74 +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) handleReply(ev *matrix.Event, rmsg config.Message) bool {
relationInterface, present := ev.Content["m.relates_to"]
if !present {
return false
}
var relation InReplyToRelation
if err := interface2Struct(relationInterface, &relation); err != nil {
// probably fine
return false
}
body := rmsg.Text
for strings.HasPrefix(body, "> ") {
lineIdx := strings.IndexRune(body, '\n')
if lineIdx == -1 {
body = ""
} else {
body = body[(lineIdx + 1):]
}
}
rmsg.Text = body
rmsg.ParentID = relation.InReplyTo.EventID
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 {
@ -435,14 +172,16 @@ 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, // Text must be a string
Account: b.Account, if rmsg.Text, ok = ev.Content["body"].(string); !ok {
UserID: ev.Sender, b.Log.Errorf("Content[body] is not a string: %T\n%#v",
ID: ev.ID, ev.Content["body"], ev.Content)
Avatar: b.getAvatarURL(ev.Sender), return
} }
// Remove homeserver suffix if configured // Remove homeserver suffix if configured
@ -460,28 +199,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
return return
} }
// Text must be a string
if rmsg.Text, ok = ev.Content["body"].(string); !ok {
b.Log.Errorf("Content[body] is not a string: %T\n%#v",
ev.Content["body"], ev.Content)
return
}
// Do we have a /me action // Do we have a /me action
if ev.Content["msgtype"].(string) == "m.emote" { if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EventUserAction rmsg.Event = config.EventUserAction
} }
// Is it an edit?
if b.handleEdit(ev, rmsg) {
return
}
// Is it a reply?
if b.handleReply(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)
@ -492,11 +214,6 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account) b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg b.Remote <- rmsg
// not crucial, so no ratelimit check here
if err := b.mc.MarkRead(ev.RoomID, ev.ID); err != nil {
b.Log.Errorf("couldn't mark message as read %s", err.Error())
}
} }
} }
@ -571,30 +288,26 @@ 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.retry(func() error { return
_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment) }
if fi.Comment != "" {
return err _, 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)))
var res *matrix.RespMediaUpload
err = b.retry(func() error {
res, err = b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
return err
})
if err != nil { if err != nil {
b.Log.Errorf("file upload failed: %#v", err) b.Log.Errorf("file upload failed: %#v", err)
return return
@ -603,60 +316,32 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
switch { switch {
case strings.Contains(mtype, "video"): case strings.Contains(mtype, "video"):
b.Log.Debugf("sendVideo %s", res.ContentURI) b.Log.Debugf("sendVideo %s", res.ContentURI)
err = b.retry(func() error {
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI) _, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
return err
})
if err != nil { if err != nil {
b.Log.Errorf("sendVideo failed: %#v", err) b.Log.Errorf("sendVideo failed: %#v", err)
} }
case strings.Contains(mtype, "image"): case strings.Contains(mtype, "image"):
b.Log.Debugf("sendImage %s", res.ContentURI) b.Log.Debugf("sendImage %s", res.ContentURI)
err = b.retry(func() error {
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI) _, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
return err
})
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.retry(func() error {
_, 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)),
},
})
return err
})
if err != nil {
b.Log.Errorf("sendAudio failed: %#v", err)
}
default:
b.Log.Debugf("sendFile %s", res.ContentURI)
err = b.retry(func() error {
_, 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)),
},
})
return err
})
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,9 +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"
matterclient6 "github.com/matterbridge/matterclient" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/v5/model"
model6 "github.com/mattermost/mattermost-server/v6/model"
) )
// handleDownloadAvatar downloads the avatar of userid from channel // handleDownloadAvatar downloads the avatar of userid from channel
@ -23,26 +21,12 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
} }
if _, ok := b.avatarMap[userid]; !ok { if _, ok := b.avatarMap[userid]; !ok {
var ( data, resp := b.mc.Client.GetProfileImage(userid, "")
data []byte
err error
resp *model.Response
)
if b.mc6 != nil {
data, _, err = b.mc6.Client.GetProfileImage(userid, "")
if err != nil {
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err)
return
}
} else {
data, resp = b.mc.Client.GetProfileImage(userid, "")
if resp.Error != nil { if resp.Error != nil {
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error) b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
return return
} }
} err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
err = helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
if err != nil { if err != nil {
b.Log.Error(err) b.Log.Error(err)
return return
@ -54,10 +38,6 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
// handleDownloadFile handles file download // handleDownloadFile handles file download
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error { func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
if b.mc6 != nil {
return b.handleDownloadFile6(rmsg, id)
}
url, _ := b.mc.Client.GetFileLink(id) url, _ := b.mc.Client.GetFileLink(id)
finfo, resp := b.mc.Client.GetFileInfo(id) finfo, resp := b.mc.Client.GetFileInfo(id)
if resp.Error != nil { if resp.Error != nil {
@ -75,25 +55,6 @@ func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error
return nil return nil
} }
// nolint:wrapcheck
func (b *Bmattermost) handleDownloadFile6(rmsg *config.Message, id string) error {
url, _, _ := b.mc6.Client.GetFileLink(id)
finfo, _, err := b.mc6.Client.GetFileInfo(id)
if err != nil {
return err
}
err = helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
if err != nil {
return err
}
data, _, err := b.mc6.Client.DownloadFile(id, true)
if err != nil {
return err
}
helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
return nil
}
func (b *Bmattermost) handleMatter() { func (b *Bmattermost) handleMatter() {
messages := make(chan *config.Message) messages := make(chan *config.Message)
if b.GetString("WebhookBindAddress") != "" { if b.GetString("WebhookBindAddress") != "" {
@ -105,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
@ -126,12 +83,6 @@ func (b *Bmattermost) handleMatter() {
} }
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
if b.mc6 != nil {
b.Log.Debug("starting matterclient6")
b.handleMatterClient6(messages)
return
}
for message := range b.mc.MessageChan { for message := range b.mc.MessageChan {
b.Log.Debugf("%#v", message.Raw.Data) b.Log.Debugf("%#v", message.Raw.Data)
@ -153,7 +104,7 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
Channel: message.Channel, Channel: message.Channel,
Text: message.Text, Text: message.Text,
ID: message.Post.Id, ID: message.Post.Id,
ParentID: message.Post.RootId, // ParentID is obsolete with mattermost ParentID: message.Post.ParentId,
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
} }
@ -185,61 +136,6 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
} }
} }
// nolint:cyclop
func (b *Bmattermost) handleMatterClient6(messages chan *config.Message) {
for message := range b.mc6.MessageChan {
b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType())
if b.skipMessage6(message) {
b.Log.Debugf("Skipped message: %#v", message)
continue
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
b.handleDownloadAvatar(message.UserID, message.Channel)
}
b.Log.Debugf("== Receiving event %#v", message)
rmsg := &config.Message{
Username: message.Username,
UserID: message.UserID,
Channel: message.Channel,
Text: message.Text,
ID: message.Post.Id,
ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
Extra: make(map[string][]interface{}),
}
// handle mattermost post properties (override username and attachments)
b.handleProps6(rmsg, message)
// create a text for bridges that don't support native editing
if message.Raw.EventType() == model6.WebsocketEventPostEdited && !b.GetBool("EditDisable") {
rmsg.Text = message.Text + b.GetString("EditSuffix")
}
if message.Raw.EventType() == model6.WebsocketEventPostDeleted {
rmsg.Event = config.EventMsgDelete
}
for _, id := range message.Post.FileIds {
err := b.handleDownloadFile(rmsg, id)
if err != nil {
b.Log.Errorf("download failed: %s", err)
}
}
// Use nickname instead of username if defined
if nick := b.mc6.GetNickName(rmsg.UserID); nick != "" {
rmsg.Username = nick
}
messages <- rmsg
}
}
func (b *Bmattermost) handleMatterHook(messages chan *config.Message) { func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
for { for {
message := b.mh.Receive() message := b.mh.Receive()
@ -255,10 +151,6 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
// handleUploadFile handles native upload of files // handleUploadFile handles native upload of files
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) { func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
if b.mc6 != nil {
return b.handleUploadFile6(msg)
}
var err error var err error
var res, id string var res, id string
channelID := b.mc.GetChannelId(msg.Channel, b.TeamID) channelID := b.mc.GetChannelId(msg.Channel, b.TeamID)
@ -277,26 +169,6 @@ func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
return res, err return res, err
} }
// nolint:forcetypeassert,wrapcheck
func (b *Bmattermost) handleUploadFile6(msg *config.Message) (string, error) {
var err error
var res, id string
channelID := b.mc6.GetChannelID(msg.Channel, b.TeamID)
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
id, err = b.mc6.UploadFile(*fi.Data, channelID, fi.Name)
if err != nil {
return "", err
}
msg.Text = fi.Comment
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
res, err = b.mc6.PostMessageWithFiles(channelID, msg.Text, msg.ParentID, []string{id})
}
return res, err
}
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) { func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
props := message.Post.Props props := message.Post.Props
if props == nil { if props == nil {
@ -321,31 +193,3 @@ func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Me
} }
} }
} }
// nolint:forcetypeassert
func (b *Bmattermost) handleProps6(rmsg *config.Message, message *matterclient6.Message) {
props := message.Post.Props
if props == nil {
return
}
if _, ok := props["override_username"].(string); ok {
rmsg.Username = props["override_username"].(string)
}
if _, ok := props["attachments"].([]interface{}); ok {
rmsg.Extra["attachments"] = props["attachments"].([]interface{})
if rmsg.Text != "" {
return
}
for _, attachment := range rmsg.Extra["attachments"] {
attach := attachment.(map[string]interface{})
if attach["text"].(string) != "" {
rmsg.Text += attach["text"].(string)
continue
}
if attach["fallback"].(string) != "" {
rmsg.Text += attach["fallback"].(string)
}
}
}
}

View File

@ -1,16 +1,13 @@
package bmattermost package bmattermost
import ( import (
"net/http"
"strings" "strings"
"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/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
matterclient6 "github.com/matterbridge/matterclient" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/v5/model"
model6 "github.com/mattermost/mattermost-server/v6/model"
) )
func (b *Bmattermost) doConnectWebhookBind() error { func (b *Bmattermost) doConnectWebhookBind() error {
@ -18,47 +15,25 @@ func (b *Bmattermost) doConnectWebhookBind() error {
case b.GetString("WebhookURL") != "": case b.GetString("WebhookURL") != "":
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"), b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{ matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), BindAddress: b.GetString("WebhookBindAddress")})
BindAddress: b.GetString("WebhookBindAddress"),
})
case b.GetString("Token") != "": case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending)") b.Log.Info("Connecting using token (sending)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin() err := b.apiLogin()
if err != nil { if err != nil {
return err return err
} }
}
case b.GetString("Login") != "": case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending)") b.Log.Info("Connecting using login/password (sending)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin() err := b.apiLogin()
if err != nil { if err != nil {
return err return err
} }
}
default: default:
b.Log.Info("Connecting using webhookbindaddress (receiving)") b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"), b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{ matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), BindAddress: b.GetString("WebhookBindAddress")})
BindAddress: b.GetString("WebhookBindAddress"),
})
} }
return nil return nil
} }
@ -66,41 +41,21 @@ func (b *Bmattermost) doConnectWebhookBind() error {
func (b *Bmattermost) doConnectWebhookURL() error { func (b *Bmattermost) doConnectWebhookURL() error {
b.Log.Info("Connecting using webhookurl (sending)") b.Log.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.GetString("WebhookURL"), b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{ matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), DisableServer: true})
DisableServer: true,
})
if b.GetString("Token") != "" { if b.GetString("Token") != "" {
b.Log.Info("Connecting using token (receiving)") b.Log.Info("Connecting using token (receiving)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin() err := b.apiLogin()
if err != nil { if err != nil {
return err return err
} }
}
} else if b.GetString("Login") != "" { } else if b.GetString("Login") != "" {
b.Log.Info("Connecting using login/password (receiving)") b.Log.Info("Connecting using login/password (receiving)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin() err := b.apiLogin()
if err != nil { if err != nil {
return err return err
} }
} }
}
return nil return nil
} }
@ -115,7 +70,6 @@ func (b *Bmattermost) apiLogin() error {
b.mc.SetLogLevel("debug") b.mc.SetLogLevel("debug")
} }
b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify") b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify")
b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck")
b.mc.NoTLS = b.GetBool("NoTLS") b.mc.NoTLS = b.GetBool("NoTLS")
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server")) b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
err := b.mc.Login() err := b.mc.Login()
@ -129,31 +83,6 @@ func (b *Bmattermost) apiLogin() error {
return nil return nil
} }
// nolint:wrapcheck
func (b *Bmattermost) apiLogin6() error {
password := b.GetString("Password")
if b.GetString("Token") != "" {
password = "token=" + b.GetString("Token")
}
b.mc6 = matterclient6.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"), "")
if b.GetBool("debug") {
b.mc6.SetLogLevel("debug")
}
b.mc6.SkipTLSVerify = b.GetBool("SkipTLSVerify")
b.mc6.SkipVersionCheck = b.GetBool("SkipVersionCheck")
b.mc6.NoTLS = b.GetBool("NoTLS")
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
if err := b.mc6.Login(); err != nil {
return err
}
b.Log.Info("Connection succeeded")
b.TeamID = b.mc6.GetTeamID()
return nil
}
// replaceAction replace the message with the correct action (/me) code // replaceAction replace the message with the correct action (/me) code
func (b *Bmattermost) replaceAction(text string) (string, bool) { func (b *Bmattermost) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") { if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
@ -293,86 +222,3 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
} }
return false return false
} }
// skipMessages returns true if this message should not be handled
// nolint:gocyclo,cyclop
func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool {
// Handle join/leave
if message.Type == "system_join_leave" ||
message.Type == "system_join_channel" ||
message.Type == "system_leave_channel" {
if b.GetBool("nosendjoinpart") {
return true
}
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{
Username: "system",
Text: message.Text,
Channel: message.Channel,
Account: b.Account,
Event: config.EventJoinLeave,
}
return true
}
// Handle edited messages
if (message.Raw.EventType() == model6.WebsocketEventPostEdited) && b.GetBool("EditDisable") {
return true
}
// Ignore non-post messages
if message.Post == nil {
b.Log.Debugf("ignoring nil message.Post: %#v", message)
return true
}
// Ignore messages sent from matterbridge
if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
b.Log.Debug("sent by matterbridge, ignoring")
return true
}
}
// Ignore messages sent from a user logged in as the bot
if b.mc6.User.Username == message.Username {
b.Log.Debug("message from same user as bot, ignoring")
return true
}
// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
if message.Post.HasReactions {
return true
}
// ignore messages from other teams than ours
if message.Raw.GetData()["team_id"].(string) != b.TeamID {
b.Log.Debug("message from other team, ignoring")
return true
}
// only handle posted, edited or deleted events
if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model6.WebsocketEventPostEdited ||
message.Raw.EventType() == model6.WebsocketEventPostDeleted) {
return true
}
return false
}
func (b *Bmattermost) getVersion() string {
proto := "https"
if b.GetBool("notls") {
proto = "http"
}
resp, err := http.Get(proto + "://" + b.GetString("server"))
if err != nil {
b.Log.Error("failed getting version")
return ""
}
defer resp.Body.Close()
return resp.Header.Get("X-Version-Id")
}

View File

@ -3,22 +3,18 @@ package bmattermost
import ( import (
"errors" "errors"
"fmt" "fmt"
"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"
"github.com/42wim/matterbridge/matterclient" "github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
matterclient6 "github.com/matterbridge/matterclient"
"github.com/rs/xid" "github.com/rs/xid"
) )
type Bmattermost struct { type Bmattermost struct {
mh *matterhook.Client mh *matterhook.Client
mc *matterclient.MMClient mc *matterclient.MMClient
mc6 *matterclient6.Client
v6 bool
uuid string uuid string
TeamID string TeamID string
*bridge.Config *bridge.Config
@ -29,10 +25,7 @@ const mattermostPlugin = "mattermost.plugin"
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
b := &Bmattermost{Config: cfg, avatarMap: make(map[string]string)} b := &Bmattermost{Config: cfg, avatarMap: make(map[string]string)}
b.v6 = b.GetBool("v6")
b.uuid = xid.New().String() b.uuid = xid.New().String()
return b return b
} }
@ -44,13 +37,6 @@ func (b *Bmattermost) Connect() error {
if b.Account == mattermostPlugin { if b.Account == mattermostPlugin {
return nil return nil
} }
if strings.HasPrefix(b.getVersion(), "6.") {
if !b.v6 {
b.v6 = true
}
}
if b.GetString("WebhookBindAddress") != "" { if b.GetString("WebhookBindAddress") != "" {
if err := b.doConnectWebhookBind(); err != nil { if err := b.doConnectWebhookBind(); err != nil {
return err return err
@ -67,35 +53,17 @@ func (b *Bmattermost) Connect() error {
return nil return nil
case b.GetString("Token") != "": case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending and receiving)") b.Log.Info("Connecting using token (sending and receiving)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin() err := b.apiLogin()
if err != nil { if err != nil {
return err return err
} }
}
go b.handleMatter() go b.handleMatter()
case b.GetString("Login") != "": case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending and receiving)") b.Log.Info("Connecting using login/password (sending and receiving)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin() err := b.apiLogin()
if err != nil { if err != nil {
return err return err
} }
}
go b.handleMatter() go b.handleMatter()
} }
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" && if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" &&
@ -115,20 +83,10 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
} }
// we can only join channels using the API // we can only join channels using the API
if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" { if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" {
var id string id := b.mc.GetChannelId(channel.Name, b.TeamID)
if b.mc6 != nil {
id = b.mc6.GetChannelID(channel.Name, b.TeamID)
} else {
id = b.mc.GetChannelId(channel.Name, b.TeamID)
}
if id == "" { if id == "" {
return fmt.Errorf("Could not find channel ID for channel %s", channel.Name) return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
} }
if b.mc6 != nil {
return b.mc6.JoinChannel(id) // nolint:wrapcheck
}
return b.mc.JoinChannel(id) return b.mc.JoinChannel(id)
} }
return nil return nil
@ -160,53 +118,22 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
if msg.ID == "" { if msg.ID == "" {
return "", nil return "", nil
} }
if b.mc6 != nil {
return msg.ID, b.mc6.DeleteMessage(msg.ID) // nolint:wrapcheck
}
return msg.ID, b.mc.DeleteMessage(msg.ID) return msg.ID, b.mc.DeleteMessage(msg.ID)
} }
// Handle prefix hint for unthreaded messages. // Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() { if msg.ParentID == "msg-parent-not-found" {
msg.ParentID = "" msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
} }
// we only can reply to the root of the thread, not to a specific ID (like discord for example does)
if msg.ParentID != "" {
if b.mc6 != nil {
post, _, err := b.mc6.Client.GetPost(msg.ParentID, "")
if err != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
}
if post.RootId != "" {
msg.ParentID = post.RootId
}
} else {
post, res := b.mc.Client.GetPost(msg.ParentID, "")
if res.Error != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, res.Error.DetailedError)
}
if post.RootId != "" {
msg.ParentID = post.RootId
}
}
}
// 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 b.mc6 != nil {
if _, err := b.mc6.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
b.Log.Errorf("PostMessage failed: %s", err)
}
} else {
if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil { if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
b.Log.Errorf("PostMessage failed: %s", err) b.Log.Errorf("PostMessage failed: %s", err)
} }
} }
}
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg) return b.handleUploadFile(&msg)
} }
@ -219,17 +146,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
// Edit message if we have an ID // Edit message if we have an ID
if msg.ID != "" { if msg.ID != "" {
if b.mc6 != nil {
return b.mc6.EditMessage(msg.ID, msg.Text) // nolint:wrapcheck
}
return b.mc.EditMessage(msg.ID, msg.Text) return b.mc.EditMessage(msg.ID, msg.Text)
} }
// Post normal message // Post normal message
if b.mc6 != nil {
return b.mc6.PostMessage(b.mc6.GetChannelID(msg.Channel, b.TeamID), msg.Text, msg.ParentID) // nolint:wrapcheck
}
return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID) return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID)
} }

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,229 +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"}
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, 0o600)
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.ParentValid() {
return b.sendReply(msg)
}
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
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 == nil || 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,96 +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
}
// If the text message is received before receiving a ServerSync
// and UserState, Client.Self or Self.Channel are nil
if event.Client.Self == nil || event.Client.Self.Channel == nil {
b.Log.Warn("Connection bootstrap not finished, discarding text message")
return
}
// 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,262 +0,0 @@
package bmumble
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net"
"strconv"
"strings"
"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), b.GetString("MessageClipped"))
} else {
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
}
// Send the individual lines
for i := range msgLines {
// Remove unnecessary newline character, since either way we're sending it as individual lines
msgLines[i] = strings.TrimSuffix(msgLines[i], "\n")
b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
}
}

View File

@ -1,295 +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)
go func() {
for msg := range c {
msg := msg
if msg.Error != nil {
b.Log.Errorf("Fatal message poll error: %s\n", msg.Error)
return
}
// Ignore messages that are from the bot user
if msg.ActorID == b.user.User || msg.ActorType == "bridged" {
continue
}
// Handle deleting messages
if msg.MessageType == ocs.MessageSystem && msg.Parent != nil && msg.Parent.MessageType == ocs.MessageDelete {
b.handleDeletingMessage(&msg, &newRoom)
continue
}
// Handle sending messages
if msg.MessageType == ocs.MessageComment {
b.handleSendingMessage(&msg, &newRoom)
continue
}
}
}()
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
}
// Standard Message Send
if msg.Event == "" {
// Handle sending files if they are included
err := b.handleSendingFile(&msg, r)
if err != nil {
b.Log.Errorf("Could not send files in message to room %v from %v: %v", msg.Channel, msg.Username, err)
return "", nil
}
sentMessage, err := b.sendText(r, &msg, 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
}
// Message Deletion
if msg.Event == config.EventMsgDelete {
messageID, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
data, err := r.room.DeleteMessage(messageID)
if err != nil {
return "", err
}
return strconv.Itoa(data.ID), nil
}
// Message is not a type that is currently supported
return "", 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) sendText(r *Broom, msg *config.Message, text string) (*ocs.TalkRoomMessageData, error) {
messageToSend := &room.Message{Message: msg.Username + text}
if b.GetBool("SeparateDisplayName") {
messageToSend.Message = text
messageToSend.ActorDisplayName = msg.Username
}
return r.room.SendComplexMessage(messageToSend)
}
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
}
func (b *Btalk) handleSendingFile(msg *config.Message, r *Broom) error {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL == "" {
continue
}
message := ""
if fi.Comment != "" {
message += fi.Comment + " "
}
message += fi.URL
_, err := b.sendText(r, msg, message)
if err != nil {
return err
}
}
return nil
}
func (b *Btalk) handleSendingMessage(msg *ocs.TalkRoomMessageData, r *Broom) {
remoteMessage := config.Message{
Text: formatRichObjectString(msg.Message, msg.MessageParameters),
Channel: r.room.Token,
Username: DisplayName(msg, b.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)
return
}
b.Log.Debugf("<= Message is %#v", remoteMessage)
b.Remote <- remoteMessage
}
func (b *Btalk) handleDeletingMessage(msg *ocs.TalkRoomMessageData, r *Broom) {
remoteMessage := config.Message{
Event: config.EventMsgDelete,
Text: config.EventMsgDelete,
Channel: r.room.Token,
ID: strconv.Itoa(msg.Parent.ID),
Account: b.Account,
}
b.Log.Debugf("<= Message being deleted is %#v", remoteMessage)
b.Remote <- remoteMessage
}
func (b *Btalk) guestSuffix() string {
guestSuffix := " (Guest)"
if b.IsKeySet("GuestSuffix") {
guestSuffix = b.GetString("GuestSuffix")
}
return guestSuffix
}
// 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

@ -1,11 +1,7 @@
package brocketchat package brocketchat
import ( import (
"fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
) )
func (b *Brocketchat) handleRocket() { func (b *Brocketchat) handleRocket() {
@ -42,26 +38,8 @@ 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 {
message := message
// 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
if _, ok := b.cache.Get(message.ID); ok { if _, ok := b.cache.Get(message.ID); ok {
continue continue
@ -80,50 +58,10 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
Account: b.Account, Account: b.Account,
UserID: message.User.ID, UserID: message.User.ID,
ID: message.ID, ID: message.ID,
Extra: make(map[string][]interface{}),
} }
b.handleAttachments(&message, rmsg)
// 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) handleAttachments(message *models.Message, rmsg *config.Message) {
if rmsg.Text == "" {
for _, attachment := range message.Attachments {
if attachment.Title != "" {
rmsg.Text = attachment.Title + "\n"
}
if attachment.Title != "" && attachment.Text != "" {
rmsg.Text += "\n"
}
if attachment.Text != "" {
rmsg.Text += attachment.Text
}
}
}
for i := range message.Attachments {
if err := b.handleDownloadFile(rmsg, &message.Attachments[i]); err != nil {
b.Log.Errorf("Could not download incoming file: %#v", err)
}
}
}
func (b *Brocketchat) handleDownloadFile(rmsg *config.Message, file *models.Attachment) error {
downloadURL := b.GetString("server") + file.TitleLink
data, err := helper.DownloadFileAuthRocket(downloadURL, b.user.Token, b.user.ID)
if err != nil {
return fmt.Errorf("download %s failed %#v", downloadURL, err)
}
helper.HandleDownloadData(b.Log, rmsg, file.Title, rmsg.Text, downloadURL, data, b.General)
return nil
}
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 {
@ -114,11 +108,6 @@ func (b *Brocketchat) Send(msg config.Message) (string, error) {
msg.Channel = strings.TrimPrefix(msg.Channel, "#") msg.Channel = strings.TrimPrefix(msg.Channel, "#")
channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel} channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel}
// Make a action /me of the message
if msg.Event == config.EventUserAction {
msg.Text = "_" + msg.Text + "_"
}
// Delete message // Delete message
if msg.Event == config.EventMsgDelete { if msg.Event == config.EventMsgDelete {
if msg.ID == "" { if msg.ID == "" {

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,10 +86,8 @@ 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
case *slack.UserChangeEvent:
b.users.invalidateUser(ev.User.ID)
default: default:
b.Log.Debugf("Unhandled incoming event: %T", ev) b.Log.Debugf("Unhandled incoming event: %T", ev)
} }
@ -132,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 {
@ -151,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 {
@ -288,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 {
@ -13,9 +13,7 @@ type BLegacy struct {
} }
func NewLegacy(cfg *bridge.Config) bridge.Bridger { func NewLegacy(cfg *bridge.Config) bridge.Bridger {
b := &BLegacy{Bslack: newBridge(cfg)} return &BLegacy{Bslack: newBridge(cfg)}
b.legacy = true
return b
} }
func (b *BLegacy) Connect() error { func (b *BLegacy) Connect() error {

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 {
@ -32,11 +32,9 @@ type Bslack struct {
channels *channels channels *channels
users *users users *users
legacy bool
} }
const ( const (
sHello = "hello"
sChannelJoin = "channel_join" sChannelJoin = "channel_join"
sChannelLeave = "channel_leave" sChannelLeave = "channel_leave"
sChannelJoined = "channel_joined" sChannelJoined = "channel_joined"
@ -64,7 +62,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 {
@ -154,18 +151,6 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
return nil return nil
} }
// try to join a channel when in legacy
if b.legacy {
_, _, _, err := b.sc.JoinConversation(channel.Name)
if err != nil {
switch err.Error() {
case "name_taken", "restricted_action":
case "default":
return err
}
}
}
b.channels.populateChannels(false) b.channels.populateChannels(false)
channelInfo, err := b.channels.getChannel(channel.Name) channelInfo, err := b.channels.getChannel(channel.Name)
@ -178,8 +163,7 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
channel.Name = channelInfo.Name channel.Name = channelInfo.Name
} }
// we can't join a channel unless we are using legacy tokens #651 if !channelInfo.IsMember {
if !channelInfo.IsMember && !b.legacy {
return fmt.Errorf("slack integration that matterbridge is using is not member of channel '%s', please add it manually", channelInfo.Name) return fmt.Errorf("slack integration that matterbridge is using is not member of channel '%s', please add it manually", channelInfo.Name)
} }
return nil return nil
@ -195,7 +179,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, b.GetString("MessageClipped"))
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 +187,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)
@ -299,7 +282,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
} }
// Handle prefix hint for unthreaded messages. // Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() { if msg.ParentID == "msg-parent-not-found" {
msg.ParentID = "" msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
} }
@ -410,6 +393,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 +412,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 +482,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 +494,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
@ -113,12 +113,6 @@ func (b *users) populateUser(userID string) {
b.users[userID] = user b.users[userID] = user
} }
func (b *users) invalidateUser(userID string) {
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
delete(b.users, userID)
}
func (b *users) populateUsers(wait bool) { func (b *users) populateUsers(wait bool) {
b.refreshMutex.Lock() b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) { if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
@ -289,9 +283,8 @@ func (b *channels) populateChannels(wait bool) {
// We only retrieve public and private channels, not IMs // We only retrieve public and private channels, not IMs
// and MPIMs as those do not have a channel name. // and MPIMs as those do not have a channel name.
queryParams := &slack.GetConversationsParameters{ queryParams := &slack.GetConversationsParameters{
ExcludeArchived: true, ExcludeArchived: "true",
Types: []string{"public_channel,private_channel"}, Types: []string{"public_channel,private_channel"},
Limit: 1000,
} }
for { for {
channels, nextCursor, err := b.sc.GetConversations(queryParams) channels, nextCursor, err := b.sc.GetConversations(queryParams)

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

@ -2,14 +2,13 @@ package btelegram
import ( import (
"html" "html"
"path/filepath" "regexp"
"strconv" "strconv"
"strings" "strings"
"unicode/utf16"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/go-telegram-bot-api/telegram-bot-api"
) )
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message { func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
@ -39,33 +38,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) {
@ -94,7 +83,7 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
// handleUsername handles the correct setting of the username // handleUsername handles the correct setting of the username
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) { func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
if message.From != nil { if message.From != nil {
rmsg.UserID = strconv.FormatInt(message.From.ID, 10) rmsg.UserID = strconv.Itoa(message.From.ID)
if b.GetBool("UseFirstName") { if b.GetBool("UseFirstName") {
rmsg.Username = message.From.FirstName rmsg.Username = message.From.FirstName
} }
@ -105,7 +94,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)
} }
} }
@ -167,7 +156,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text) rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
// channels don't have (always?) user information. see #410 // channels don't have (always?) user information. see #410
if message.From != nil { if message.From != nil {
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.FormatInt(message.From.ID, 10), b.General) rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
} }
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
@ -180,21 +169,16 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// handleDownloadAvatar downloads the avatar of userid from channel // handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. // sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails // logs an error message if it fails
func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) { func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
rmsg := config.Message{ rmsg := config.Message{Username: "system",
Username: "system",
Text: "avatar", Text: "avatar",
Channel: channel, Channel: channel,
Account: b.Account, Account: b.Account,
UserID: strconv.FormatInt(userid, 10), UserID: strconv.Itoa(userid),
Event: config.EventAvatarDownload, Event: config.EventAvatarDownload,
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{})}
}
if _, ok := b.avatarMap[strconv.FormatInt(userid, 10)]; ok {
return
}
if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1}) photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
if err != nil { if err != nil {
b.Log.Errorf("Userprofile download failed for %#v %s", userid, err) b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
@ -203,7 +187,7 @@ func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) {
if len(photos.Photos) > 0 { if len(photos.Photos) > 0 {
photo := photos.Photos[0][0] photo := photos.Photos[0][0]
url := b.getFileDirectURL(photo.FileID) url := b.getFileDirectURL(photo.FileID)
name := strconv.FormatInt(userid, 10) + ".png" name := strconv.Itoa(userid) + ".png"
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize) b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General) err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
@ -220,35 +204,6 @@ func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) {
b.Remote <- rmsg b.Remote <- rmsg
} }
} }
func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) {
format := b.GetString("MediaConvertTgs")
if helper.SupportsFormat(format) {
b.Log.Debugf("Format supported by %s, converting %v", helper.LottieBackend(), name)
} else {
// 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
@ -274,7 +229,7 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
name = message.Document.FileName name = message.Document.FileName
text = " " + message.Document.FileName + " : " + url text = " " + message.Document.FileName + " : " + url
case message.Photo != nil: case message.Photo != nil:
photos := message.Photo photos := *message.Photo
size = photos[len(photos)-1].FileSize size = photos[len(photos)-1].FileSize
text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true) text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
} }
@ -298,18 +253,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)
} }
// rename .oga to .ogg https://github.com/42wim/matterbridge/issues/906#issuecomment-741793512
if strings.HasSuffix(name, ".oga") && message.Audio != nil {
name = strings.Replace(name, ".oga", ".ogg", 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
} }
@ -333,15 +285,11 @@ func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, err
if msg.ID == "" { if msg.ID == "" {
return "", nil return "", nil
} }
msgid, err := strconv.Atoi(msg.ID) msgid, err := strconv.Atoi(msg.ID)
if err != nil { if err != nil {
return "", err return "", err
} }
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
cfg := tgbotapi.NewDeleteMessage(chatid, msgid)
_, err = b.c.Send(cfg)
return "", err return "", err
} }
@ -363,9 +311,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")
@ -387,32 +332,21 @@ func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
Name: fi.Name, Name: fi.Name,
Bytes: *fi.Data, Bytes: *fi.Data,
} }
switch filepath.Ext(fi.Name) { re := regexp.MustCompile(".(jpg|png)$")
case ".jpg", ".jpe", ".png": if re.MatchString(fi.Name) {
pc := tgbotapi.NewPhoto(chatid, file) c = tgbotapi.NewPhotoUpload(chatid, file)
pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) } else {
c = pc c = tgbotapi.NewDocumentUpload(chatid, file)
case ".mp4", ".m4v":
vc := tgbotapi.NewVideo(chatid, file)
vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = vc
case ".mp3", ".oga":
ac := tgbotapi.NewAudio(chatid, file)
ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = ac
case ".ogg":
voc := tgbotapi.NewVoice(chatid, file)
voc.Caption, voc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = voc
default:
dc := tgbotapi.NewDocument(chatid, file)
dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = dc
} }
_, err := b.c.Send(c) _, err := b.c.Send(c)
if err != nil { if err != nil {
b.Log.Errorf("file upload failed: %#v", err) b.Log.Errorf("file upload failed: %#v", err)
} }
if fi.Comment != "" {
if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil {
b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err)
}
}
} }
return "" return ""
} }
@ -422,14 +356,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([]rune(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)
@ -441,36 +367,16 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa
if message.Entities == nil { if message.Entities == nil {
return return
} }
indexMovedBy := 0
// for now only do URL replacements // for now only do URL replacements
for _, e := range message.Entities { for _, e := range *message.Entities {
if e.Type == "text_link" { if e.Type == "text_link" {
url, err := e.ParseURL() url, err := e.ParseURL()
if err != nil { if err != nil {
b.Log.Errorf("entity text_link url parse failed: %s", err) b.Log.Errorf("entity text_link url parse failed: %s", err)
continue continue
} }
utfEncodedString := utf16.Encode([]rune(rmsg.Text)) link := rmsg.Text[e.Offset : e.Offset+e.Length]
if e.Offset+e.Length > len(utfEncodedString) { rmsg.Text = strings.Replace(rmsg.Text, link, url.String(), 1)
b.Log.Errorf("entity length is too long %d > %d", e.Offset+e.Length, len(utfEncodedString))
continue
}
link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length])
rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1)
}
if e.Type == "code" {
offset := e.Offset + indexMovedBy
rmsg.Text = rmsg.Text[:offset] + "`" + rmsg.Text[offset:offset+e.Length] + "`" + rmsg.Text[offset+e.Length:]
indexMovedBy += 2
}
if e.Type == "pre" {
offset := e.Offset + indexMovedBy
rmsg.Text = rmsg.Text[:offset] + "```\n" + rmsg.Text[offset:offset+e.Length] + "\n```" + rmsg.Text[offset+e.Length:]
indexMovedBy += 8
} }
} }
} }

View File

@ -2,21 +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/v5" "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"
) )
type Btelegram struct { type Btelegram struct {
@ -26,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 %s does not appear to work:\n%#v", tgsConvertFormat, helper.LottieBackend(), err)
}
if !helper.SupportsFormat(tgsConvertFormat) {
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s doesn't support it.", tgsConvertFormat, helper.LottieBackend())
}
}
return &Btelegram{Config: cfg, avatarMap: make(map[string]string)} return &Btelegram{Config: cfg, avatarMap: make(map[string]string)}
} }
@ -49,7 +37,11 @@ func (b *Btelegram) Connect() error {
} }
u := tgbotapi.NewUpdate(0) u := tgbotapi.NewUpdate(0)
u.Timeout = 60 u.Timeout = 60
updates := b.c.GetUpdatesChan(u) updates, err := b.c.GetUpdatesChan(u)
if err != nil {
b.Log.Debugf("%#v", err)
return err
}
b.Log.Info("Connection succeeded") b.Log.Info("Connection succeeded")
go b.handleRecv(updates) go b.handleRecv(updates)
return nil return nil
@ -63,28 +55,6 @@ func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error {
return nil return nil
} }
func TGGetParseMode(b *Btelegram, username string, text string) (textout string, parsemode string) {
textout = username + text
if b.GetString("MessageFormat") == HTMLFormat {
b.Log.Debug("Using mode HTML")
parsemode = tgbotapi.ModeHTML
}
if b.GetString("MessageFormat") == "Markdown" {
b.Log.Debug("Using mode markdown")
parsemode = tgbotapi.ModeMarkdown
}
if b.GetString("MessageFormat") == MarkdownV2 {
b.Log.Debug("Using mode MarkdownV2")
parsemode = MarkdownV2
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
textout = username + html.EscapeString(text)
parsemode = tgbotapi.ModeHTML
}
return textout, parsemode
}
func (b *Btelegram) Send(msg config.Message) (string, error) { func (b *Btelegram) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg) b.Log.Debugf("=> Receiving %#v", msg)
@ -111,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)
@ -127,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 {
@ -147,10 +110,20 @@ func (b *Btelegram) getFileDirectURL(id string) string {
func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) { func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) {
m := tgbotapi.NewMessage(chatid, "") m := tgbotapi.NewMessage(chatid, "")
m.Text, m.ParseMode = TGGetParseMode(b, username, text) m.Text = username + text
if b.GetString("MessageFormat") == HTMLFormat {
m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview") b.Log.Debug("Using mode HTML")
m.ParseMode = tgbotapi.ModeHTML
}
if b.GetString("MessageFormat") == "Markdown" {
b.Log.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
m.Text = username + html.EscapeString(text)
m.ParseMode = tgbotapi.ModeHTML
}
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,332 +0,0 @@
package bvk
import (
"bytes"
"context"
"regexp"
"strconv"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/SevereCloud/vksdk/v2/api"
"github.com/SevereCloud/vksdk/v2/events"
longpoll "github.com/SevereCloud/vksdk/v2/longpoll-bot"
"github.com/SevereCloud/vksdk/v2/object"
)
const (
audioMessage = "audio_message"
document = "doc"
photo = "photo"
video = "video"
graffiti = "graffiti"
sticker = "sticker"
wall = "wall"
)
type user struct {
lastname, firstname, avatar string
}
type Bvk struct {
c *api.VK
lp *longpoll.LongPoll
usernamesMap map[int]user // cache of user names and avatar URLs
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Bvk{usernamesMap: make(map[int]user), Config: cfg}
}
func (b *Bvk) Connect() error {
b.Log.Info("Connecting")
b.c = api.NewVK(b.GetString("Token"))
var err error
b.lp, err = longpoll.NewLongPollCommunity(b.c)
if err != nil {
b.Log.Debugf("%#v", err)
return err
}
b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) {
b.handleMessage(obj.Message, false)
})
b.Log.Info("Connection succeeded")
go func() {
err := b.lp.Run()
if err != nil {
b.Log.Fatal("Enable longpoll in group management")
}
}()
return nil
}
func (b *Bvk) Disconnect() error {
b.lp.Shutdown()
return nil
}
func (b *Bvk) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Bvk) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
peerID, err := strconv.Atoi(msg.Channel)
if err != nil {
return "", err
}
params := api.Params{}
text := msg.Username + msg.Text
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
// generate attachments string
attachment, urls := b.uploadFiles(msg.Extra, peerID)
params["attachment"] = attachment
text += urls
}
}
params["message"] = text
if msg.ID == "" {
// New message
params["random_id"] = time.Now().Unix()
params["peer_ids"] = msg.Channel
res, e := b.c.MessagesSendPeerIDs(params)
if e != nil {
return "", err
}
return strconv.Itoa(res[0].ConversationMessageID), nil
}
// Edit message
messageID, err := strconv.ParseInt(msg.ID, 10, 64)
if err != nil {
return "", err
}
params["peer_id"] = peerID
params["conversation_message_id"] = messageID
_, err = b.c.MessagesEdit(params)
if err != nil {
return "", err
}
return msg.ID, nil
}
func (b *Bvk) getUser(id int) user {
u, found := b.usernamesMap[id]
if !found {
b.Log.Debug("Fetching username for ", id)
if id >= 0 {
result, _ := b.c.UsersGet(api.Params{
"user_ids": id,
"fields": "photo_200",
})
resUser := result[0]
u = user{lastname: resUser.LastName, firstname: resUser.FirstName, avatar: resUser.Photo200}
b.usernamesMap[id] = u
} else {
result, _ := b.c.GroupsGetByID(api.Params{
"group_id": id * -1,
})
resGroup := result[0]
u = user{lastname: resGroup.Name, avatar: resGroup.Photo200}
}
}
return u
}
func (b *Bvk) handleMessage(msg object.MessagesMessage, isFwd bool) {
b.Log.Debug("ChatID: ", msg.PeerID)
// fetch user info
u := b.getUser(msg.FromID)
rmsg := config.Message{
Text: msg.Text,
Username: u.firstname + " " + u.lastname,
Avatar: u.avatar,
Channel: strconv.Itoa(msg.PeerID),
Account: b.Account,
UserID: strconv.Itoa(msg.FromID),
ID: strconv.Itoa(msg.ConversationMessageID),
Extra: make(map[string][]interface{}),
}
if msg.ReplyMessage != nil {
ur := b.getUser(msg.ReplyMessage.FromID)
rmsg.Text = "Re: " + ur.firstname + " " + ur.lastname + "\n" + rmsg.Text
}
if isFwd {
rmsg.Username = "Fwd: " + rmsg.Username
}
if len(msg.Attachments) > 0 {
urls, text := b.getFiles(msg.Attachments)
if text != "" {
rmsg.Text += "\n" + text
}
// download
b.downloadFiles(&rmsg, urls)
}
if len(msg.FwdMessages) > 0 {
rmsg.Text += strconv.Itoa(len(msg.FwdMessages)) + " forwarded messages"
}
b.Remote <- rmsg
if len(msg.FwdMessages) > 0 {
// recursive processing of forwarded messages
for _, m := range msg.FwdMessages {
m.PeerID = msg.PeerID
b.handleMessage(m, true)
}
}
}
func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, string) {
var attachments []string
text := ""
for _, f := range extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
text += fi.Comment + "\n"
}
a, err := b.uploadFile(fi, peerID)
if err != nil {
b.Log.Error("File upload error ", fi.Name)
}
attachments = append(attachments, a)
}
return strings.Join(attachments, ","), text
}
func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) {
r := bytes.NewReader(*file.Data)
photoRE := regexp.MustCompile(".(jpg|jpe|png)$")
if photoRE.MatchString(file.Name) {
p, err := b.c.UploadMessagesPhoto(peerID, r)
if err != nil {
return "", err
}
return photo + strconv.Itoa(p[0].OwnerID) + "_" + strconv.Itoa(p[0].ID), nil
}
var doctype string
if strings.Contains(file.Name, ".ogg") {
doctype = audioMessage
} else {
doctype = document
}
doc, err := b.c.UploadMessagesDoc(peerID, doctype, file.Name, "", r)
if err != nil {
return "", err
}
switch doc.Type {
case audioMessage:
return document + strconv.Itoa(doc.AudioMessage.OwnerID) + "_" + strconv.Itoa(doc.AudioMessage.ID), nil
case document:
return document + strconv.Itoa(doc.Doc.OwnerID) + "_" + strconv.Itoa(doc.Doc.ID), nil
}
return "", nil
}
func (b *Bvk) getFiles(attachments []object.MessagesMessageAttachment) ([]string, string) {
var urls []string
var text []string
for _, a := range attachments {
switch a.Type {
case photo:
var resolution float64 = 0
url := a.Photo.Sizes[0].URL
for _, size := range a.Photo.Sizes {
r := size.Height * size.Width
if resolution < r {
resolution = r
url = size.URL
}
}
urls = append(urls, url)
case document:
urls = append(urls, a.Doc.URL)
case graffiti:
urls = append(urls, a.Graffiti.URL)
case audioMessage:
urls = append(urls, a.AudioMessage.DocsDocPreviewAudioMessage.LinkOgg)
case sticker:
var resolution float64 = 0
url := a.Sticker.Images[0].URL
for _, size := range a.Sticker.Images {
r := size.Height * size.Width
if resolution < r {
resolution = r
url = size.URL
}
}
urls = append(urls, url+".png")
case video:
text = append(text, "https://vk.com/video"+strconv.Itoa(a.Video.OwnerID)+"_"+strconv.Itoa(a.Video.ID))
case wall:
text = append(text, "https://vk.com/wall"+strconv.Itoa(a.Wall.FromID)+"_"+strconv.Itoa(a.Wall.ID))
default:
text = append(text, "This attachment is not supported ("+a.Type+")")
}
}
return urls, strings.Join(text, "\n")
}
func (b *Bvk) downloadFiles(rmsg *config.Message, urls []string) {
for _, url := range urls {
data, err := helper.DownloadFile(url)
if err == nil {
urlPart := strings.Split(url, "/")
name := strings.Split(urlPart[len(urlPart)-1], "?")[0]
helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General)
}
}
}

View File

@ -1,15 +1,14 @@
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/matterbridge/go-whatsapp"
"github.com/jpillora/backoff"
whatsappExt "github.com/matterbridge/mautrix-whatsapp/whatsapp-ext"
) )
/* /*
@ -22,56 +21,12 @@ 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 b.Log.Errorf("%v", err) // TODO implement proper handling? at least respond to different error types
// ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094
if strings.Contains(err.Error(), "error processing data: received invalid data") ||
strings.Contains(err.Error(), "invalid string with tag 174") {
return
}
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
func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
if message.Info.FromMe { if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") {
return return
} }
// whatsapp sends last messages to show context , cut them // whatsapp sends last messages to show context , cut them
@ -79,17 +34,17 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
return return
} }
groupJID := message.Info.RemoteJid messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
senderJID := message.Info.SenderJid groupJid := message.Info.RemoteJid
if len(senderJID) == 0 { senderJid := message.Info.SenderJid
if message.Info.Source != nil && message.Info.Source.Participant != nil { if len(senderJid) == 0 {
senderJID = *message.Info.Source.Participant // TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
} senderJid = *message.Info.Source.Participant
} }
// translate sender's JID to the nicest username we can get // 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
} }
@ -97,284 +52,53 @@ 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
mention := b.getSenderNotify(numberAndSuffix[0] + "@s.whatsapp.net") mention := b.getSenderNotify(numberAndSuffix[0] + whatsappExt.NewUserSuffix)
if mention == "" { if mention == "" {
mention = "someone" mention = "someone"
} }
message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1) message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
} }
} }
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJid, b.Account)
rmsg := config.Message{ rmsg := config.Message{
UserID: senderJID, UserID: senderJid,
Username: senderName, Username: senderName,
Text: message.Text, Text: message.Text,
Channel: groupJID, Timestamp: messageTime,
Channel: groupJid,
Account: b.Account, Account: b.Account,
Protocol: b.Protocol, Protocol: b.Protocol,
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string // ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
ID: message.Info.Id, // Event string `json:"event"`
} // Gateway string // will be added during message processing
ID: message.Info.Id}
if avatarURL, exists := b.userAvatars[senderJID]; exists { if avatarURL, exists := b.userAvatars[senderJid]; exists {
rmsg.Avatar = avatarURL rmsg.Avatar = avatarURL
} }
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg) b.Log.Debugf("<= Message is %#v", rmsg)
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 || message.Info.Timestamp < b.startedAt { // fmt.Println(message) // TODO implement
return //}
} //
//func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
senderJID := message.Info.SenderJid // fmt.Println(message) // TODO implement
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil { //}
senderJID = *message.Info.Source.Participant //
} //func (b *Bwhatsapp) HandleJsonMessage(message string) {
// fmt.Println(message) // TODO implement
senderName := b.getSenderName(message.Info.SenderJid) //}
if senderName == "" { // TODO HandleRawMessage
senderName = "Someone" // don't expose telephone number // TODO HandleAudioMessage
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
if fileExt[0] == ".jfif" {
fileExt[0] = ".jpg"
}
// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463
if fileExt[0] == ".jpe" {
fileExt[0] = ".jpg"
}
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with type %s", filename, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download image failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleVideoMessage downloads video messages
func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
if len(fileExt) == 0 {
fileExt = append(fileExt, ".mp4")
}
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download video failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleAudioMessage downloads audio messages
func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
if len(fileExt) == 0 {
fileExt = append(fileExt, ".ogg")
}
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download audio failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleDocumentMessage downloads documents
func (b *Bwhatsapp) HandleDocumentMessage(message whatsapp.DocumentMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
filename := fmt.Sprintf("%v", message.FileName)
b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download document message failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}

View File

@ -2,28 +2,17 @@ package bwhatsapp
import ( import (
"encoding/gob" "encoding/gob"
"encoding/json"
"errors" "errors"
"fmt"
"os" "os"
"strings"
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go" qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
"github.com/Rhymen/go-whatsapp" "github.com/matterbridge/go-whatsapp"
) )
type ProfilePicInfo struct {
URL string `json:"eurl"`
Tag string `json:"tag"`
Status int16 `json:"status"`
}
func qrFromTerminal(invert bool) chan string { func qrFromTerminal(invert bool) chan string {
qr := make(chan string) qr := make(chan string)
go func() { go func() {
terminal := qrcodeTerminal.New() terminal := qrcodeTerminal.New()
if invert { if invert {
terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium) terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium)
} }
@ -46,12 +35,13 @@ func (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
if err != nil { if err != nil {
return session, err return session, err
} }
defer file.Close() defer file.Close()
decoder := gob.NewDecoder(file) decoder := gob.NewDecoder(file)
err = decoder.Decode(&session)
return session, decoder.Decode(&session) if err != nil {
return session, err
}
return session, nil
} }
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error { func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
@ -66,31 +56,11 @@ func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
if err != nil { if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
encoder := gob.NewEncoder(file) encoder := gob.NewEncoder(file)
err = encoder.Encode(session)
return encoder.Encode(session) return err
}
func (b *Bwhatsapp) restoreSession() (*whatsapp.Session, error) {
session, err := b.readSession()
if err != nil {
b.Log.Warn(err.Error())
}
b.Log.Debugln("Restoring WhatsApp session..")
session, err = b.conn.RestoreWithSession(session)
if err != nil {
// restore session connection timed out (I couldn't get over it without logging in again)
return nil, errors.New("failed to restore session: " + err.Error())
}
b.Log.Debugln("Session restored successfully!")
return &session, nil
} }
func (b *Bwhatsapp) getSenderName(senderJid string) string { func (b *Bwhatsapp) getSenderName(senderJid string) string {
@ -101,33 +71,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 ""
} }
@ -135,29 +80,5 @@ func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
if sender, exists := b.users[senderJid]; exists { if sender, exists := b.users[senderJid]; exists {
return sender.Notify return sender.Notify
} }
return "" return ""
} }
func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
data, err := b.conn.GetProfilePicThumb(jid)
if err != nil {
return nil, fmt.Errorf("failed to get avatar: %v", err)
}
content := <-data
info := &ProfilePicInfo{}
err = json.Unmarshal([]byte(content), info)
if err != nil {
return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
}
return info, nil
}
func isGroupJid(identifier string) bool {
return strings.HasSuffix(identifier, "@g.us") ||
strings.HasSuffix(identifier, "@temp") ||
strings.HasSuffix(identifier, "@broadcast")
}

View File

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

@ -1,14 +1,8 @@
package bxmpp package bxmpp
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings" "strings"
"sync"
"time" "time"
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
@ -20,36 +14,50 @@ import (
) )
type Bxmpp struct { type Bxmpp struct {
*bridge.Config
startTime time.Time
xc *xmpp.Client xc *xmpp.Client
xmppMap map[string]string xmppMap map[string]string
connected bool *bridge.Config
sync.RWMutex startTime time.Time
avatarAvailability map[string]bool
avatarMap map[string]string
} }
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
return &Bxmpp{ b := &Bxmpp{Config: cfg}
Config: cfg, b.xmppMap = make(map[string]string)
xmppMap: make(map[string]string), return b
avatarAvailability: make(map[string]bool),
avatarMap: make(map[string]string),
}
} }
func (b *Bxmpp) Connect() error { func (b *Bxmpp) Connect() error {
var err error
b.Log.Infof("Connecting %s", b.GetString("Server")) b.Log.Infof("Connecting %s", b.GetString("Server"))
if err := b.createXMPP(); err != nil { b.xc, err = b.createXMPP()
if err != nil {
b.Log.Debugf("%#v", err) b.Log.Debugf("%#v", err)
return err return err
} }
b.Log.Info("Connection succeeded") b.Log.Info("Connection succeeded")
go b.manageConnection() go func() {
initial := true
bf := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
for {
if initial {
b.handleXMPP()
initial = false
}
d := bf.Duration()
b.Log.Infof("Disconnected. Reconnecting in %s", d)
time.Sleep(d)
b.xc, err = b.createXMPP()
if err == nil {
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels}
b.handleXMPP()
bf.Reset()
}
}
}()
return nil return nil
} }
@ -68,190 +76,58 @@ func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error {
} }
func (b *Bxmpp) Send(msg config.Message) (string, error) { func (b *Bxmpp) Send(msg config.Message) (string, error) {
// should be fixed by using a cache instead of dropping
if !b.Connected() {
return "", fmt.Errorf("bridge %s not connected, dropping message %#v to bridge", b.Account, msg)
}
// ignore delete messages // ignore delete messages
if msg.Event == config.EventMsgDelete { if msg.Event == config.EventMsgDelete {
return "", nil return "", nil
} }
b.Log.Debugf("=> Receiving %#v", msg) b.Log.Debugf("=> Receiving %#v", msg)
if msg.Event == config.EventAvatarDownload { // Upload a file (in xmpp case send the upload URL because xmpp has no native upload support)
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).
var err error
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.Log.Debugf("=> Sending attachement message %#v", rmsg) b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: rmsg.Channel + "@" + b.GetString("Muc"), Text: rmsg.Username + rmsg.Text})
if b.GetString("WebhookURL") != "" {
err = b.postSlackCompatibleWebhook(msg)
} else {
_, err = b.xc.Send(xmpp.Chat{
Type: "groupchat",
Remote: rmsg.Channel + "@" + b.GetString("Muc"),
Text: rmsg.Username + rmsg.Text,
})
}
if err != nil {
b.Log.WithError(err).Error("Unable to send message with share URL.")
}
} }
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
return "", b.handleUploadFile(&msg) return b.handleUploadFile(&msg)
} }
} }
if b.GetString("WebhookURL") != "" { var msgreplaceid string
b.Log.Debugf("Sending message using Webhook") msgid := xid.New().String()
err := b.postSlackCompatibleWebhook(msg)
if err != nil {
b.Log.Errorf("Failed to send message using webhook: %s", err)
return "", err
}
return "", nil
}
// Post normal message.
var msgReplaceID string
msgID := xid.New().String()
if msg.ID != "" { if msg.ID != "" {
msgReplaceID = msg.ID msgid = msg.ID
msgreplaceid = msg.ID
} }
b.Log.Debugf("=> Sending message %#v", msg) // Post normal message
if _, err := b.xc.Send(xmpp.Chat{ _, err := b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text, ID: msgid, ReplaceID: msgreplaceid})
Type: "groupchat", if err != nil {
Remote: msg.Channel + "@" + b.GetString("Muc"),
Text: msg.Username + msg.Text,
ID: msgID,
ReplaceID: msgReplaceID,
}); err != nil {
return "", err return "", err
} }
return msgID, nil return msgid, nil
} }
func (b *Bxmpp) postSlackCompatibleWebhook(msg config.Message) error { func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
type XMPPWebhook struct { tc := new(tls.Config)
Username string `json:"username"` tc.InsecureSkipVerify = b.GetBool("SkipTLSVerify")
Text string `json:"text"` tc.ServerName = strings.Split(b.GetString("Server"), ":")[0]
}
webhookBody, err := json.Marshal(XMPPWebhook{
Username: msg.Username,
Text: msg.Text,
})
if err != nil {
b.Log.Errorf("Failed to marshal webhook: %s", err)
return err
}
resp, err := http.Post(b.GetString("WebhookURL")+"/"+url.QueryEscape(msg.Channel), "application/json", bytes.NewReader(webhookBody))
if err != nil {
b.Log.Errorf("Failed to POST webhook: %s", err)
return err
}
resp.Body.Close()
return nil
}
func (b *Bxmpp) createXMPP() error {
var serverName string
switch {
case !b.GetBool("Anonymous"):
if !strings.Contains(b.GetString("Jid"), "@") {
return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid"))
}
serverName = strings.Split(b.GetString("Jid"), "@")[1]
case !strings.Contains(b.GetString("Server"), ":"):
serverName = strings.Split(b.GetString("Server"), ":")[0]
default:
serverName = b.GetString("Server")
}
tc := &tls.Config{
ServerName: serverName,
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()
return err return b.xc, err
}
func (b *Bxmpp) manageConnection() {
b.setConnected(true)
initial := true
bf := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
// Main connection loop. Each iteration corresponds to a successful
// connection attempt and the subsequent handling of the connection.
for {
if initial {
initial = false
} else {
b.Remote <- config.Message{
Username: "system",
Text: "rejoin",
Channel: "",
Account: b.Account,
Event: config.EventRejoinChannels,
}
}
if err := b.handleXMPP(); err != nil {
b.Log.WithError(err).Error("Disconnected.")
b.setConnected(false)
}
// Reconnection loop using an exponential back-off strategy. We
// only break out of the loop if we have successfully reconnected.
for {
d := bf.Duration()
b.Log.Infof("Reconnecting in %s.", d)
time.Sleep(d)
b.Log.Infof("Reconnecting now.")
if err := b.createXMPP(); err == nil {
b.setConnected(true)
bf.Reset()
break
}
b.Log.Warn("Failed to reconnect.")
}
}
} }
func (b *Bxmpp) xmppKeepAlive() chan bool { func (b *Bxmpp) xmppKeepAlive() chan bool {
@ -263,7 +139,8 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
select { select {
case <-ticker.C: case <-ticker.C:
b.Log.Debugf("PING") b.Log.Debugf("PING")
if err := b.xc.PingC2S("", ""); err != nil { err := b.xc.PingC2S("", "")
if err != nil {
b.Log.Debugf("PING failed %#v", err) b.Log.Debugf("PING failed %#v", err)
} }
case <-done: case <-done:
@ -275,80 +152,53 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
} }
func (b *Bxmpp) handleXMPP() error { func (b *Bxmpp) handleXMPP() error {
var ok bool
var msgid string
b.startTime = time.Now() b.startTime = time.Now()
done := b.xmppKeepAlive() done := b.xmppKeepAlive()
defer close(done) defer close(done)
for { for {
m, err := b.xc.Recv() m, err := b.xc.Recv()
if err != nil { if err != nil {
// An error together with AvatarData is non-fatal
switch m.(type) {
case xmpp.AvatarData:
continue
default:
return err return err
} }
}
switch v := m.(type) { switch v := m.(type) {
case xmpp.Chat: case xmpp.Chat:
if v.Type == "groupchat" { if v.Type == "groupchat" {
b.Log.Debugf("== Receiving %#v", v) b.Log.Debugf("== Receiving %#v", v)
event := ""
// Skip invalid messages. // skip invalid messages
if b.skipMessage(v) { if b.skipMessage(v) {
continue continue
} }
var event string
if strings.Contains(v.Text, "has set the subject to:") { if strings.Contains(v.Text, "has set the subject to:") {
event = config.EventTopicChange event = config.EventTopicChange
} }
msgid = v.ID
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
if v.ReplaceID != "" { if v.ReplaceID != "" {
msgID = v.ReplaceID msgid = v.ReplaceID
} }
rmsg := config.Message{ rmsg := config.Message{
Username: b.parseNick(v.Remote), Username: b.parseNick(v.Remote),
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,
} }
// Check if we have an action event. // check if we have an action event
var ok bool
rmsg.Text, ok = b.replaceAction(rmsg.Text) rmsg.Text, ok = b.replaceAction(rmsg.Text)
if ok { if ok {
rmsg.Event = config.EventUserAction rmsg.Event = config.EventUserAction
} }
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg) b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
} }
case xmpp.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
} }
} }
} }
@ -361,46 +211,35 @@ func (b *Bxmpp) replaceAction(text string) (string, bool) {
} }
// handleUploadFile handles native upload of files // handleUploadFile handles native upload of files
func (b *Bxmpp) handleUploadFile(msg *config.Message) error { func (b *Bxmpp) handleUploadFile(msg *config.Message) (string, error) {
var urlDesc string var urldesc = ""
for _, file := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fileInfo := file.(config.FileInfo) fi := f.(config.FileInfo)
if fileInfo.Comment != "" { if fi.Comment != "" {
msg.Text += fileInfo.Comment + ": " msg.Text += fi.Comment + ": "
} }
if fileInfo.URL != "" { if fi.URL != "" {
msg.Text = fileInfo.URL msg.Text = fi.URL
if fileInfo.Comment != "" { if fi.Comment != "" {
msg.Text = fileInfo.Comment + ": " + fileInfo.URL msg.Text = fi.Comment + ": " + fi.URL
urlDesc = fileInfo.Comment urldesc = fi.Comment
} }
} }
if _, err := b.xc.Send(xmpp.Chat{ _, err := b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text})
Type: "groupchat", if err != nil {
Remote: msg.Channel + "@" + b.GetString("Muc"), return "", err
Text: msg.Username + msg.Text,
}); err != nil {
return err
} }
if fi.URL != "" {
if fileInfo.URL != "" { b.xc.SendOOB(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Ooburl: fi.URL, Oobdesc: urldesc})
if _, err := b.xc.SendOOB(xmpp.Chat{
Type: "groupchat",
Remote: msg.Channel + "@" + b.GetString("Muc"),
Ooburl: fileInfo.URL,
Oobdesc: urlDesc,
}); err != nil {
b.Log.WithError(err).Warn("Failed to send share URL.")
} }
} }
} return "", nil
return nil
} }
func (b *Bxmpp) parseNick(remote string) string { func (b *Bxmpp) parseNick(remote string) string {
s := strings.Split(remote, "@") s := strings.Split(remote, "@")
if len(s) > 1 { if len(s) > 0 {
s = strings.Split(s[1], "/") s = strings.Split(s[1], "/")
if len(s) == 2 { if len(s) == 2 {
return s[1] // nick return s[1] // nick
@ -439,23 +278,7 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
return true return true
} }
// Ignore messages posted by our webhook
if b.GetString("WebhookURL") != "" && strings.Contains(message.ID, "webhookbot") {
return true
}
// skip delayed messages // skip delayed messages
return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5 t := time.Time{}
} return message.Stamp != t
func (b *Bxmpp) setConnected(state bool) {
b.Lock()
b.connected = state
defer b.Unlock()
}
func (b *Bxmpp) Connected() bool {
b.RLock()
defer b.RUnlock()
return b.connected
} }

View File

@ -2,7 +2,6 @@ package bzulip
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"strconv" "strconv"
"strings" "strings"
@ -12,7 +11,6 @@ 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/42wim/matterbridge/version"
gzb "github.com/matterbridge/gozulipbot" gzb "github.com/matterbridge/gozulipbot"
) )
@ -29,7 +27,7 @@ func New(cfg *bridge.Config) bridge.Bridger {
} }
func (b *Bzulip) Connect() error { func (b *Bzulip) Connect() error {
bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login"), UserAgent: fmt.Sprintf("matterbridge/%s", version.Release)} bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login")}
bot.Init() bot.Init()
q, err := bot.RegisterAll() q, err := bot.RegisterAll()
b.q = q b.q = q
@ -127,7 +125,6 @@ func (b *Bzulip) handleQueue() error {
b.Log.Debug("heartbeat received.") b.Log.Debug("heartbeat received.")
default: default:
b.Log.Debugf("receiving error: %#v", err) b.Log.Debugf("receiving error: %#v", err)
time.Sleep(time.Second * 10)
} }
if err != nil { if err != nil {
continue continue
@ -138,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)
} }
} }

File diff suppressed because it is too large Load Diff

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

View File

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

View File

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

View File

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

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,12 +0,0 @@
//go:build !noharmony
// +build !noharmony
package bridgemap
import (
bharmony "github.com/42wim/matterbridge/bridge/harmony"
)
func init() {
FullMap["harmony"] = bharmony.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,36 @@ package bridgemap
import ( import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/api"
"github.com/42wim/matterbridge/bridge/discord"
"github.com/42wim/matterbridge/bridge/gitter"
"github.com/42wim/matterbridge/bridge/irc"
"github.com/42wim/matterbridge/bridge/matrix"
"github.com/42wim/matterbridge/bridge/mattermost"
"github.com/42wim/matterbridge/bridge/rocketchat"
"github.com/42wim/matterbridge/bridge/slack"
"github.com/42wim/matterbridge/bridge/sshchat"
"github.com/42wim/matterbridge/bridge/steam"
"github.com/42wim/matterbridge/bridge/telegram"
"github.com/42wim/matterbridge/bridge/whatsapp"
"github.com/42wim/matterbridge/bridge/xmpp"
"github.com/42wim/matterbridge/bridge/zulip"
) )
var ( var FullMap = map[string]bridge.Factory{
FullMap = map[string]bridge.Factory{} "api": api.New,
UserTypingSupport = map[string]struct{}{} "discord": bdiscord.New,
) "gitter": bgitter.New,
"irc": birc.New,
"mattermost": bmattermost.New,
"matrix": bmatrix.New,
"rocketchat": brocketchat.New,
"slack-legacy": bslack.NewLegacy,
"slack": bslack.New,
"sshchat": bsshchat.New,
"steam": bsteam.New,
"telegram": btelegram.New,
"whatsapp": bwhatsapp.New,
"xmpp": bxmpp.New,
"zulip": bzulip.New,
}

View File

@ -1,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 !novk
package bridgemap
import (
bvk "github.com/42wim/matterbridge/bridge/vk"
)
func init() {
FullMap["vk"] = bvk.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"
@ -10,11 +9,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/d5/tengo/script"
"github.com/d5/tengo/v2" "github.com/d5/tengo/stdlib"
"github.com/d5/tengo/v2/stdlib"
lru "github.com/hashicorp/golang-lru" lru "github.com/hashicorp/golang-lru"
"github.com/kyokomi/emoji/v2" "github.com/peterhellberg/emojilib"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -66,7 +64,7 @@ func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway {
func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string { func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
ID := protocol + " " + mID ID := protocol + " " + mID
if gw.Messages.Contains(ID) { if gw.Messages.Contains(ID) {
return ID return mID
} }
// If not keyed, iterate through cache for downstream, and infer upstream. // If not keyed, iterate through cache for downstream, and infer upstream.
@ -75,7 +73,7 @@ func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
ids := v.([]*BrMsgID) ids := v.([]*BrMsgID)
for _, downstreamMsgObj := range ids { for _, downstreamMsgObj := range ids {
if ID == downstreamMsgObj.ID { if ID == downstreamMsgObj.ID {
return mid.(string) return strings.Replace(mid.(string), protocol+" ", "", 1)
} }
} }
} }
@ -86,7 +84,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 +103,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 +291,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 +300,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]
@ -337,21 +322,15 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri
} }
i++ i++
} }
nick = strings.ReplaceAll(nick, "{NOPINGNICK}", msg.Username[:i]+"\u200b"+msg.Username[i:]) nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
} }
nick = strings.ReplaceAll(nick, "{BRIDGE}", br.Name) nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.ReplaceAll(nick, "{PROTOCOL}", br.Protocol) nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
nick = strings.ReplaceAll(nick, "{GATEWAY}", gw.Name) nick = strings.Replace(nick, "{GATEWAY}", gw.Name, -1)
nick = strings.ReplaceAll(nick, "{LABEL}", br.GetString("Label")) nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1)
nick = strings.ReplaceAll(nick, "{NICK}", msg.Username) nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.ReplaceAll(nick, "{USERID}", msg.UserID) nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1)
nick = strings.ReplaceAll(nick, "{CHANNEL}", msg.Channel)
tengoNick, err := gw.modifyUsernameTengo(msg, br)
if err != nil {
gw.logger.Errorf("modifyUsernameTengo error: %s", err)
}
nick = strings.ReplaceAll(nick, "{TENGO}", tengoNick)
return nick return nick
} }
@ -365,29 +344,12 @@ 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)
} }
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)
}
// replace :emoji: to unicode // replace :emoji: to unicode
emoji.ReplacePadding = "" msg.Text = emojilib.Replace(msg.Text)
msg.Text = emoji.Sprint(msg.Text)
br := gw.Bridges[msg.Account] br := gw.Bridges[msg.Account]
// loop to replace messages // loop to replace messages
@ -432,15 +394,9 @@ func (gw *Gateway) SendMessage(
} }
} }
// Only send irc notices to irc
if msg.Event == config.EventNoticeIRC && dest.Protocol != "irc" {
return "", nil
}
// 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
@ -454,40 +410,23 @@ func (gw *Gateway) SendMessage(
msg.Channel = rmsg.Channel msg.Channel = rmsg.Channel
} }
msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel) msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel)
if msg.ParentID == "" { if msg.ParentID == "" {
msg.ParentID = strings.Replace(canonicalParentMsgID, dest.Protocol+" ", "", 1) msg.ParentID = canonicalParentMsgID
} }
// if the parentID is still empty and we have a parentID set in the original message // if the parentID is still empty and we have a parentID set in the original message
// this means that we didn't find it in the cache so set it to a "msg-parent-not-found" constant // this means that we didn't find it in the cache so set it "msg-parent-not-found"
if msg.ParentID == "" && rmsg.ParentID != "" { if msg.ParentID == "" && rmsg.ParentID != "" {
msg.ParentID = config.ParentIDNotFound msg.ParentID = "msg-parent-not-found"
} }
drop, err := gw.modifyOutMessageTengo(rmsg, &msg, dest)
if err != nil {
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
@ -539,7 +478,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
} }
@ -547,11 +486,10 @@ 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)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("msgAccount", msg.Account) _ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel) _ = s.Add("msgChannel", msg.Channel)
c, err := s.Compile() c, err := s.Compile()
@ -565,90 +503,3 @@ func modifyInMessageTengo(filename string, msg *config.Message) error {
msg.Username = c.Get("msgUsername").String() msg.Username = c.Get("msgUsername").String()
return nil return nil
} }
func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) (string, error) {
filename := gw.BridgeValues().Tengo.RemoteNickFormat
if filename == "" {
return "", nil
}
res, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
s := tengo.NewScript(res)
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("result", "")
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("nick", msg.Username)
_ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel)
_ = s.Add("channel", msg.Channel)
_ = s.Add("msgProtocol", msg.Protocol)
_ = s.Add("remoteAccount", br.Account)
_ = s.Add("protocol", br.Protocol)
_ = s.Add("bridge", br.Name)
_ = s.Add("gateway", gw.Name)
c, err := s.Compile()
if err != nil {
return "", err
}
if err := c.Run(); err != nil {
return "", err
}
return c.Get("result").String(), nil
}
func (gw *Gateway) modifyOutMessageTengo(origmsg *config.Message, msg *config.Message, br *bridge.Bridge) (bool, error) {
filename := gw.BridgeValues().Tengo.OutMessage
var (
res []byte
err error
drop bool
)
if filename == "" {
res, err = internal.Asset("tengo/outmessage.tengo")
if err != nil {
return drop, err
}
} else {
res, err = ioutil.ReadFile(filename)
if err != nil {
return drop, err
}
}
s := tengo.NewScript(res)
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("inAccount", origmsg.Account)
_ = s.Add("inProtocol", origmsg.Protocol)
_ = s.Add("inChannel", origmsg.Channel)
_ = s.Add("inGateway", origmsg.Gateway)
_ = s.Add("inEvent", origmsg.Event)
_ = s.Add("outAccount", br.Account)
_ = s.Add("outProtocol", br.Protocol)
_ = s.Add("outChannel", msg.Channel)
_ = s.Add("outGateway", gw.Name)
_ = s.Add("outEvent", msg.Event)
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("msgDrop", drop)
c, err := s.Compile()
if err != nil {
return drop, err
}
if err := c.Run(); err != nil {
return drop, err
}
drop = c.Get("msgDrop").Bool()
msg.Text = c.Get("msgText").String()
msg.Username = c.Get("msgUsername").String()
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

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

View File

@ -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
} }
@ -110,9 +104,7 @@ func (r *Router) disableBridge(br *bridge.Bridge, err error) bool {
if r.BridgeValues().General.IgnoreFailureOnStart { if r.BridgeValues().General.IgnoreFailureOnStart {
r.logger.Error(err) r.logger.Error(err)
// setting this bridge empty // setting this bridge empty
*br = bridge.Bridge{ *br = bridge.Bridge{}
Log: br.Log,
}
return true return true
} }
return false return false
@ -133,11 +125,7 @@ func (r *Router) handleReceive() {
r.handleEventGetChannelMembers(&msg) r.handleEventGetChannelMembers(&msg)
r.handleEventFailure(&msg) r.handleEventFailure(&msg)
r.handleEventRejoinChannels(&msg) r.handleEventRejoinChannels(&msg)
idx := 0
// Set message protocol based on the account it came from
msg.Protocol = r.getBridge(msg.Account).Protocol
filesHandled := false
for _, gw := range r.Gateways { for _, gw := range r.Gateways {
// record all the message ID's of the different bridges // record all the message ID's of the different bridges
var msgIDs []*BrMsgID var msgIDs []*BrMsgID
@ -146,26 +134,17 @@ func (r *Router) handleReceive() {
} }
msg.Timestamp = time.Now() msg.Timestamp = time.Now()
gw.modifyMessage(&msg) gw.modifyMessage(&msg)
if !filesHandled { if idx == 0 {
gw.handleFiles(&msg) gw.handleFiles(&msg)
filesHandled = true
} }
for _, br := range gw.Bridges { for _, br := range gw.Bridges {
msgIDs = append(msgIDs, gw.handleMessage(&msg, br)...) msgIDs = append(msgIDs, gw.handleMessage(&msg, br)...)
} }
// only add the message ID if it doesn't already exists
if msg.ID != "" { if _, ok := gw.Messages.Get(msg.Protocol + " " + msg.ID); !ok && msg.ID != "" {
_, exists := gw.Messages.Get(msg.Protocol + " " + msg.ID)
// Only add the message ID if it doesn't already exist
//
// For some bridges we always add/update the message ID.
// This is necessary as msgIDs will change if a bridge returns
// a different ID in response to edits.
if !exists {
gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs) gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs)
} }
} idx++
} }
} }
} }

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