forked from lug/matterbridge
		
	Compare commits
	
		
			11 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 6d8cccccf1 | ||
|   | dd63438e7b | ||
|   | 5f5d6c6e8a | ||
|   | aefa8a9341 | ||
|   | 1c3e764d57 | ||
|   | 0b03076a9d | ||
|   | 1e6a2bc8f7 | ||
|   | 82fe80e52f | ||
|   | db012bd9b7 | ||
|   | dd2374158b | ||
|   | 6693157258 | 
| @@ -1,2 +0,0 @@ | |||||||
| Dockerfile |  | ||||||
| tgs.Dockerfile |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| go: |  | ||||||
|   comments: |  | ||||||
|     disabled: true |  | ||||||
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/Bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/Bug_report.md
									
									
									
									
										vendored
									
									
								
							| @@ -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 |  | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/Feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/Feature_request.md
									
									
									
									
										vendored
									
									
								
							| @@ -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 |  | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										71
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										71
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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 |  | ||||||
							
								
								
									
										58
									
								
								.github/workflows/development.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										58
									
								
								.github/workflows/development.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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: v1.29 |  | ||||||
|           args: "-v --new-from-rev HEAD~5" |  | ||||||
|   test-build-upload: |  | ||||||
|     strategy: |  | ||||||
|       matrix: |  | ||||||
|         go-version: [1.15.x, 1.16.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) |  | ||||||
|         GOOS=linux GOARCH=amd64 go build -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64 |  | ||||||
|         GOOS=windows GOARCH=amd64 go build -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe |  | ||||||
|         GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64 |  | ||||||
|     - name: Upload linux 64-bit |  | ||||||
|       if: startsWith(matrix.go-version,'1.16') |  | ||||||
|       uses: actions/upload-artifact@v2 |  | ||||||
|       with: |  | ||||||
|         name: matterbridge-linux-64bit |  | ||||||
|         path: output/lin |  | ||||||
|     - name: Upload windows 64-bit |  | ||||||
|       if: startsWith(matrix.go-version,'1.16') |  | ||||||
|       uses: actions/upload-artifact@v2 |  | ||||||
|       with: |  | ||||||
|         name: matterbridge-windows-64bit |  | ||||||
|         path: output/win |  | ||||||
|     - name: Upload darwin 64-bit |  | ||||||
|       if: startsWith(matrix.go-version,'1.16') |  | ||||||
|       uses: actions/upload-artifact@v2 |  | ||||||
|       with: |  | ||||||
|         name: matterbridge-darwin-64bit |  | ||||||
|         path: output/mac |  | ||||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +0,0 @@ | |||||||
| # Exclude matterbridge binary |  | ||||||
| /matterbridge |  | ||||||
| /matterbridge.exe |  | ||||||
|  |  | ||||||
| # Exclude configuration file |  | ||||||
| matterbridge.toml |  | ||||||
| @@ -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,15 +174,7 @@ linters: | |||||||
|     - lll |     - lll | ||||||
|     - maligned |     - maligned | ||||||
|     - prealloc |     - prealloc | ||||||
|     - wsl |  | ||||||
|     - gomnd |  | ||||||
|     - godox |  | ||||||
|     - goerr113 |  | ||||||
|     - testpackage |  | ||||||
|     - godot |  | ||||||
|     - interfacer |  | ||||||
|     - goheader |  | ||||||
|     - noctx |  | ||||||
|  |  | ||||||
| # rules to deal with reported isues | # rules to deal with reported isues | ||||||
| issues: | issues: | ||||||
|   | |||||||
| @@ -21,18 +21,14 @@ builds: | |||||||
|   ldflags: |   ldflags: | ||||||
|     - -s -w -X main.githash={{.ShortCommit}} |     - -s -w -X main.githash={{.ShortCommit}} | ||||||
|  |  | ||||||
| archives: | archive: | ||||||
|   - |   name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" | ||||||
|     id: matterbridge |   format: binary | ||||||
|     builds: |   files: | ||||||
|     - matterbridge |     - none* | ||||||
|     name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" |   replacements: | ||||||
|     format: binary |     386: 32bit | ||||||
|     files: |     amd64: 64bit | ||||||
|       - none* |  | ||||||
|     replacements: |  | ||||||
|       386: 32bit |  | ||||||
|       amd64: 64bit |  | ||||||
|  |  | ||||||
| checksum: | checksum: | ||||||
|   name_template: 'checksums.txt' |   name_template: 'checksums.txt' | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								.travis.yml
									
									
									
									
									
										Normal 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=" | ||||||
							
								
								
									
										23
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -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 \ | ||||||
|         && go build -mod vendor -ldflags "-X main.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"] |  | ||||||
|   | |||||||
							
								
								
									
										371
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										371
									
								
								README.md
									
									
									
									
									
								
							| @@ -3,35 +3,32 @@ | |||||||
| # matterbridge | # matterbridge | ||||||
|  |  | ||||||
| <br /> | <br /> | ||||||
| **A simple chat bridge**<br /> |    **A simple chat bridge**<br /> | ||||||
| Letting people be where they want to be.<br /> |    Letting people be where they want to be.<br /> | ||||||
| <sub>Bridges between a growing number of protocols. Click below to demo or join the development chat.</sub> |    <sub>Bridges between a growing number of protocols. Click below to demo or join the development chat.</sub> | ||||||
|  |  | ||||||
|    <sup> |    <sup> | ||||||
|  |  | ||||||
| [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] | | ||||||
| [MSTeams][mb-msteams] | |       [Rocket.Chat][mb-rocketchat] | | ||||||
| [Rocket.Chat][mb-rocketchat] | |       [XMPP][mb-xmpp] | | ||||||
| [Slack][mb-slack] | |       [Twitch][mb-twitch] | | ||||||
| [Telegram][mb-telegram] | |       [WhatsApp][mb-whatsapp] | | ||||||
| [Twitch][mb-twitch] | |       [Zulip][mb-zulip] | | ||||||
| [WhatsApp][mb-whatsapp] | |       [Telegram][mb-telegram] | | ||||||
| [XMPP][mb-xmpp] | |       And more... | ||||||
| [Zulip][mb-zulip] | |    </sup> | ||||||
| And more... |  | ||||||
| </sup> |  | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
| [](https://github.com/42wim/matterbridge/releases/latest) | [](https://github.com/42wim/matterbridge/releases/latest) | ||||||
| [](https://codeclimate.com/github/42wim/matterbridge/maintainability) |    [](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion) | ||||||
| [](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br /> |    [](https://codeclimate.com/github/42wim/matterbridge/maintainability) | ||||||
|  |    [](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br /> | ||||||
|   <hr /> |   <hr /> | ||||||
| </div> | </div> | ||||||
| <div align="right"><sup> | <div align="right"><sup> | ||||||
| @@ -44,172 +41,126 @@ 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) |  | ||||||
|   - [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.22.1](https://github.com/42wim/matterbridge/releases/latest) | * Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/) | ||||||
| - Development releases (follows master) can be downloaded [here](https://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 1.13+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed. |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| go get github.com/42wim/matterbridge | go get github.com/42wim/matterbridge | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| You should now have matterbridge binary in the ~/go/bin directory: | You should now have matterbridge binary in the bin directory: | ||||||
|  |  | ||||||
| ```bash | ``` | ||||||
| $ ls ~/go/bin/ | $ ls bin/ | ||||||
| matterbridge | matterbridge | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Configuration | ## Configuration | ||||||
|  |  | ||||||
| ### Basic configuration | ### Basic configuration | ||||||
|  |  | ||||||
| See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. | See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. | ||||||
|  |  | ||||||
| ### Settings |  | ||||||
|  |  | ||||||
| All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for each bridge. |  | ||||||
|  |  | ||||||
| ### Advanced configuration | ### Advanced configuration | ||||||
|  | * [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example. | ||||||
| - [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example. |  | ||||||
|  |  | ||||||
| ### Examples | ### Examples | ||||||
|  |  | ||||||
| #### Bridge mattermost (off-topic) - irc (#testing) | #### Bridge mattermost (off-topic) - irc (#testing) | ||||||
|  |  | ||||||
| ```toml | ```toml | ||||||
| [irc] | [irc] | ||||||
|     [irc.freenode] |     [irc.freenode] | ||||||
| @@ -238,7 +189,6 @@ enable=true | |||||||
| ``` | ``` | ||||||
|  |  | ||||||
| #### Bridge slack (#general) - discord (general) | #### Bridge slack (#general) - discord (general) | ||||||
|  |  | ||||||
| ```toml | ```toml | ||||||
| [slack] | [slack] | ||||||
| [slack.test] | [slack.test] | ||||||
| @@ -270,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") | ||||||
| @@ -283,11 +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 | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ## 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 | ||||||
| @@ -295,35 +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/> |  | ||||||
| - Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc) |  | ||||||
|  |  | ||||||
| ## Thanks | ## Thanks | ||||||
|  |  | ||||||
| @@ -335,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://webchat.freenode.net/?channels=matterbridgechat | ||||||
| [mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat |    [mb-discord]: https://discord.gg/AkKPtrQ | ||||||
| [mb-keybase]: https://keybase.io/team/matterbridge |    [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-xmpp]: https://inverse.chat/ | ||||||
| [mb-slack]: https://join.slack.com/t/matterbridgechat/shared_invite/zt-2ourq2h2-7YvyYBq2WFGC~~zEzA68_Q |    [mb-twitch]: https://www.twitch.tv/matterbridge | ||||||
| [mb-telegram]: https://t.me/Matterbridge |    [mb-whatsapp]: https://www.whatsapp.com/ | ||||||
| [mb-twitch]: https://www.twitch.tv/matterbridge |    [mb-zulip]: https://matterbridge.zulipchat.com/register/ | ||||||
| [mb-whatsapp]: https://www.whatsapp.com/ |    [mb-telegram]: https://t.me/Matterbridge | ||||||
| [mb-xmpp]: https://inverse.chat/ |  | ||||||
| [mb-zulip]: https://matterbridge.zulipchat.com/register/ |  | ||||||
|   | |||||||
| @@ -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 |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -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) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -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 | ||||||
| @@ -89,29 +76,23 @@ type Protocol struct { | |||||||
| 	BindAddress            string // mattermost, slack // DEPRECATED | 	BindAddress            string // mattermost, slack // DEPRECATED | ||||||
| 	Buffer                 int    // api | 	Buffer                 int    // api | ||||||
| 	Charset                string // irc | 	Charset                string // irc | ||||||
| 	ClientID               string // msteams |  | ||||||
| 	ColorNicks             bool   // only irc for now | 	ColorNicks             bool   // only irc for now | ||||||
| 	Debug                  bool   // general | 	Debug                  bool   // general | ||||||
| 	DebugLevel             int    // only for irc now | 	DebugLevel             int    // only for irc now | ||||||
| 	DisableWebPagePreview  bool   // telegram |  | ||||||
| 	EditSuffix             string // mattermost, slack, discord, telegram, gitter | 	EditSuffix             string // mattermost, slack, discord, telegram, gitter | ||||||
| 	EditDisable            bool   // mattermost, slack, discord, telegram, gitter | 	EditDisable            bool   // mattermost, slack, discord, telegram, gitter | ||||||
| 	HTMLDisable            bool   // matrix |  | ||||||
| 	IconURL                string // mattermost, slack | 	IconURL                string // mattermost, slack | ||||||
| 	IgnoreFailureOnStart   bool   // general | 	IgnoreFailureOnStart   bool   // general | ||||||
| 	IgnoreNicks            string // all protocols | 	IgnoreNicks            string // all protocols | ||||||
| 	IgnoreMessages         string // all protocols | 	IgnoreMessages         string // all protocols | ||||||
| 	Jid                    string // xmpp | 	Jid                    string // xmpp | ||||||
| 	JoinDelay              string // all protocols |  | ||||||
| 	Label                  string // all protocols | 	Label                  string // all protocols | ||||||
| 	Login                  string // mattermost, matrix | 	Login                  string // mattermost, matrix | ||||||
| 	LogFile                string // general |  | ||||||
| 	MediaDownloadBlackList []string | 	MediaDownloadBlackList []string | ||||||
| 	MediaDownloadPath      string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server. | 	MediaDownloadPath      string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server. | ||||||
| 	MediaDownloadSize      int    // all protocols | 	MediaDownloadSize      int    // all protocols | ||||||
| 	MediaServerDownload    string | 	MediaServerDownload    string | ||||||
| 	MediaServerUpload      string | 	MediaServerUpload      string | ||||||
| 	MediaConvertTgs        string     // telegram |  | ||||||
| 	MediaConvertWebPToPNG  bool       // telegram | 	MediaConvertWebPToPNG  bool       // telegram | ||||||
| 	MessageDelay           int        // IRC, time in millisecond to wait between messages | 	MessageDelay           int        // IRC, time in millisecond to wait between messages | ||||||
| 	MessageFormat          string     // telegram | 	MessageFormat          string     // telegram | ||||||
| @@ -128,46 +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 |  | ||||||
| 	RejoinDelay            int        // IRC | 	RejoinDelay            int        // IRC | ||||||
| 	ReplaceMessages        [][]string // all protocols | 	ReplaceMessages        [][]string // all protocols | ||||||
| 	ReplaceNicks           [][]string // all protocols | 	ReplaceNicks           [][]string // all protocols | ||||||
| 	RemoteNickFormat       string     // all protocols | 	RemoteNickFormat       string     // all protocols | ||||||
| 	RunCommands            []string   // IRC | 	RunCommands            []string   // irc | ||||||
| 	Server                 string     // IRC,mattermost,XMPP,discord | 	Server                 string     // IRC,mattermost,XMPP,discord | ||||||
| 	SessionFile            string     // msteams,whatsapp |  | ||||||
| 	ShowJoinPart           bool       // all protocols | 	ShowJoinPart           bool       // all protocols | ||||||
| 	ShowTopicChange        bool       // slack | 	ShowTopicChange        bool       // slack | ||||||
| 	ShowUserTyping         bool       // slack | 	ShowUserTyping         bool       // slack | ||||||
| 	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 |  | ||||||
| 	TenantID               string     // msteams |  | ||||||
| 	Token                  string     // gitter, slack, discord, api | 	Token                  string     // gitter, slack, discord, api | ||||||
| 	Topic                  string     // zulip | 	Topic                  string     // zulip | ||||||
| 	URL                    string     // mattermost, slack // DEPRECATED | 	URL                    string     // mattermost, slack // DEPRECATED | ||||||
| 	UseAPI                 bool       // mattermost, slack | 	UseAPI                 bool       // mattermost, slack | ||||||
| 	UseLocalAvatar         []string   // discord |  | ||||||
| 	UseSASL                bool       // IRC | 	UseSASL                bool       // IRC | ||||||
| 	UseTLS                 bool       // IRC | 	UseTLS                 bool       // IRC | ||||||
| 	UseDiscriminator       bool       // discord | 	UseDiscriminator       bool       // discord | ||||||
| 	UseFirstName           bool       // telegram | 	UseFirstName           bool       // telegram | ||||||
| 	UseUserName            bool       // discord, matrix | 	UseUserName            bool       // discord | ||||||
| 	UseInsecureURL         bool       // telegram | 	UseInsecureURL         bool       // telegram | ||||||
| 	VerboseJoinPart        bool       // IRC |  | ||||||
| 	WebhookBindAddress     string     // mattermost, slack | 	WebhookBindAddress     string     // mattermost, slack | ||||||
| 	WebhookURL             string     // mattermost, slack | 	WebhookURL             string     // mattermost, slack | ||||||
| } | } | ||||||
| @@ -193,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 | ||||||
| @@ -223,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) | ||||||
| @@ -260,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 | ||||||
| 	} | 	} | ||||||
| @@ -281,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, | ||||||
| @@ -324,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() | ||||||
| @@ -393,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 { | ||||||
|   | |||||||
| @@ -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 | ||||||
| @@ -20,9 +20,12 @@ 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 { | 			b.channels, err = b.c.GuildChannels(guild.ID) | ||||||
| 			continue | 			b.guildID = guild.ID | ||||||
|  | 			guildFound = true | ||||||
|  | 			if err != nil { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// 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) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("could not get %#v's channels: %w", b.GetString("Server"), err) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		b.guildID = guild.ID |  | ||||||
| 	} | 	} | ||||||
| 	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)") |  | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 	b.channelsMutex.RLock() | ||||||
| 	// Legacy note: WebhookURL used to have an actual webhook URL that we would edit, | 	if b.GetString("WebhookURL") == "" { | ||||||
| 	// but we stopped doing that due to Discord making rate limits more aggressive. | 		for _, channel := range b.channels { | ||||||
| 	// | 			b.Log.Debugf("found channel %#v", channel) | ||||||
| 	// Even older: the same WebhookURL used to be used by every channel, which is usually unexpected. | 		} | ||||||
| 	// This is no longer possible. | 	} else { | ||||||
| 	if b.GetString("WebhookURL") != "" { | 		b.canEditWebhooks = true | ||||||
| 		message := "The global WebhookURL setting has been removed. " | 		for _, channel := range b.channels { | ||||||
| 		message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. " | 			b.Log.Debugf("found channel %#v; verifying PermissionManageWebhooks", channel) | ||||||
| 		message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections." | 			perms, permsErr := b.c.State.UserChannelPermissions(userinfo.ID, channel.ID) | ||||||
| 		b.Log.Errorln(message) | 			manageWebhooks := discordgo.PermissionManageWebhooks | ||||||
| 		return fmt.Errorf("use of removed WebhookURL setting") | 			if permsErr != nil || perms&manageWebhooks != manageWebhooks { | ||||||
| 	} | 				b.Log.Warnf("Can't manage webhooks in channel \"%s\"", channel.Name) | ||||||
|  | 				b.canEditWebhooks = false | ||||||
| 	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 |  | ||||||
| 		} | 		} | ||||||
|  | 		if b.canEditWebhooks { | ||||||
| 		whID, whToken, ok := b.splitURL(channel.Options.WebhookURL) | 			b.Log.Info("Can manage webhooks; will edit channel for global webhook on send") | ||||||
| 		if !ok { | 		} else { | ||||||
| 			return fmt.Errorf("failed to parse WebhookURL %#v for channel %#v", channel.Options.WebhookURL, channel.ID) | 			b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send") | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		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 { |  | ||||||
| 			b.Log.WithError(err).Println("transmitter could not refresh guild webhooks") |  | ||||||
| 			return err |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	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 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		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 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return b.handleEventBotUser(&msg, channelID) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // 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,7 +275,7 @@ 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) | 			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) | ||||||
| @@ -290,7 +283,7 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st | |||||||
| 		} | 		} | ||||||
| 		// 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) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -303,25 +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, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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 | ||||||
|   | |||||||
| @@ -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 |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -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) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -6,10 +6,10 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"unicode" | 	"unicode" | ||||||
|  |  | ||||||
| 	"github.com/matterbridge/discordgo" | 	"github.com/bwmarrin/discordgo" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string { | func (b *Bdiscord) getNick(user *discordgo.User) string { | ||||||
| 	b.membersMutex.RLock() | 	b.membersMutex.RLock() | ||||||
| 	defer b.membersMutex.RUnlock() | 	defer b.membersMutex.RUnlock() | ||||||
|  |  | ||||||
| @@ -23,9 +23,9 @@ func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If we didn't find nick, search for it. | 	// If we didn't find nick, search for it. | ||||||
| 	member, err := b.c.GuildMember(guildID, user.ID) | 	member, err := b.c.GuildMember(b.guildID, user.ID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		b.Log.Warnf("Failed to fetch information for member %#v on guild %#v: %s", user, guildID, err) | 		b.Log.Warnf("Failed to fetch information for member %#v: %s", user, err) | ||||||
| 		return user.Username | 		return user.Username | ||||||
| 	} else if member == nil { | 	} else if member == nil { | ||||||
| 		b.Log.Warnf("Got no information for member %#v", user) | 		b.Log.Warnf("Got no information for member %#v", user) | ||||||
| @@ -51,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() | ||||||
|  |  | ||||||
| @@ -62,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" | ||||||
| @@ -183,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 | ||||||
| @@ -204,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 { | ||||||
|   | |||||||
| @@ -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) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -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 |  | ||||||
| } |  | ||||||
| @@ -1,147 +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, |  | ||||||
| 			}, |  | ||||||
| 		) |  | ||||||
| 		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 := "" |  | ||||||
| 			if msg.Text == "" { |  | ||||||
| 				content = fi.Comment |  | ||||||
| 			} |  | ||||||
| 			_, e2 := b.transmitter.Send( |  | ||||||
| 				channelID, |  | ||||||
| 				&discordgo.WebhookParams{ |  | ||||||
| 					Username:  msg.Username, |  | ||||||
| 					AvatarURL: msg.Avatar, |  | ||||||
| 					File:      &file, |  | ||||||
| 					Content:   content, |  | ||||||
| 				}, |  | ||||||
| 			) |  | ||||||
| 			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) |  | ||||||
| 	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, |  | ||||||
| 		}) |  | ||||||
| 		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 |  | ||||||
| } |  | ||||||
| @@ -5,10 +5,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"image/png" | 	"image/png" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" |  | ||||||
| 	"os/exec" |  | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -17,10 +14,8 @@ import ( | |||||||
| 	"golang.org/x/image/webp" | 	"golang.org/x/image/webp" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/gomarkdown/markdown" |  | ||||||
| 	"github.com/gomarkdown/markdown/html" |  | ||||||
| 	"github.com/gomarkdown/markdown/parser" |  | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"gitlab.com/golang-commonmark/markdown" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // DownloadFile downloads the given non-authenticated URL. | // DownloadFile downloads the given non-authenticated URL. | ||||||
| @@ -51,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. | ||||||
| @@ -205,21 +176,15 @@ func ClipMessage(text string, length int) string { | |||||||
| 	return text | 	return text | ||||||
| } | } | ||||||
|  |  | ||||||
| // ParseMarkdown takes in an input string as markdown and parses it to html |  | ||||||
| func ParseMarkdown(input string) string { | func ParseMarkdown(input string) string { | ||||||
| 	extensions := parser.HardLineBreak | parser.NoIntraEmphasis | 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) | ||||||
| @@ -234,66 +199,3 @@ func ConvertWebPToPNG(data *[]byte) error { | |||||||
| 	*data = w.Bytes() | 	*data = w.Bytes() | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // CanConvertTgsToX Checks whether the external command necessary for ConvertTgsToX works. |  | ||||||
| func CanConvertTgsToX() error { |  | ||||||
| 	// We depend on the fact that `lottie_convert.py --help` has exit status 0. |  | ||||||
| 	// Hyrum's Law predicted this, and Murphy's Law predicts that this will break eventually. |  | ||||||
| 	// However, there is no alternative like `lottie_convert.py --is-properly-installed` |  | ||||||
| 	cmd := exec.Command("lottie_convert.py", "--help") |  | ||||||
| 	return cmd.Run() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ConvertTgsToWebP convert input data (which should be tgs format) to WebP format |  | ||||||
| // This relies on an external command, which is ugly, but works. |  | ||||||
| func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error { |  | ||||||
| 	// lottie can't handle input from a pipe, so write to a temporary file: |  | ||||||
| 	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 |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -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" | ||||||
|  |  | ||||||
| @@ -54,12 +55,12 @@ func (b *Birc) handleFiles(msg *config.Message) bool { | |||||||
| 	for _, f := range msg.Extra["file"] { | 	for _, f := range msg.Extra["file"] { | ||||||
| 		fi := f.(config.FileInfo) | 		fi := f.(config.FileInfo) | ||||||
| 		if fi.Comment != "" { | 		if fi.Comment != "" { | ||||||
| 			msg.Text += fi.Comment + " : " | 			msg.Text += fi.Comment + ": " | ||||||
| 		} | 		} | ||||||
| 		if fi.URL != "" { | 		if fi.URL != "" { | ||||||
| 			msg.Text = fi.URL | 			msg.Text = fi.URL | ||||||
| 			if fi.Comment != "" { | 			if fi.Comment != "" { | ||||||
| 				msg.Text = fi.Comment + " : " + fi.URL | 				msg.Text = fi.Comment + ": " + fi.URL | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event} | 		b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event} | ||||||
| @@ -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 | ||||||
| 		} | 		} | ||||||
|  | 		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} | 		msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave} | ||||||
| 		if b.GetBool("verbosejoinpart") { |  | ||||||
| 			b.Log.Debugf("<= Sending verbose JOIN_LEAVE event from %s to gateway", b.Account) |  | ||||||
| 			msg = config.Message{Username: "system", Text: event.Source.Name + " (" + event.Source.Ident + "@" + event.Source.Host + ") " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave} |  | ||||||
| 		} else { |  | ||||||
| 			b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account) |  | ||||||
| 		} |  | ||||||
| 		b.Log.Debugf("<= Message is %#v", msg) | 		b.Log.Debugf("<= Message is %#v", msg) | ||||||
| 		b.Remote <- msg | 		b.Remote <- msg | ||||||
| 		return | 		return | ||||||
| @@ -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 == "" { | ||||||
|   | |||||||
| @@ -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,10 +156,6 @@ func (b *Birc) Send(msg config.Message) (string, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var msgLines []string | 	var msgLines []string | ||||||
| 	if b.GetBool("StripMarkdown") { |  | ||||||
| 		msg.Text = stripmd.Strip(msg.Text) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if b.GetBool("MessageSplit") { | 	if b.GetBool("MessageSplit") { | ||||||
| 		msgLines = helper.GetSubLines(msg.Text, b.MessageLength) | 		msgLines = helper.GetSubLines(msg.Text, b.MessageLength) | ||||||
| 	} else { | 	} else { | ||||||
| @@ -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,58 +199,22 @@ 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 | 		if b.GetBool("Colornicks") { | ||||||
| 		// https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md | 			checksum := crc32.ChecksumIEEE([]byte(msg.Username)) | ||||||
| 		// nolint:nestif | 			colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes | ||||||
| 		if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) && | 			username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) | ||||||
| 			b.GetBool("UseRelayMsg") { | 		} | ||||||
| 			username = sanitizeNick(username) | 		if msg.Event == config.EventUserAction { | ||||||
| 			text := msg.Text | 			b.i.Cmd.Action(msg.Channel, username+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 { | 		} else { | ||||||
| 			if b.GetBool("Colornicks") { | 			b.Log.Debugf("Sending to channel %s", msg.Channel) | ||||||
| 				checksum := crc32.ChecksumIEEE([]byte(msg.Username)) | 			b.i.Cmd.Message(msg.Channel, username+msg.Text) | ||||||
| 				colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes |  | ||||||
| 				username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) |  | ||||||
| 			} |  | ||||||
| 			switch msg.Event { |  | ||||||
| 			case config.EventUserAction: |  | ||||||
| 				b.i.Cmd.Action(msg.Channel, username+msg.Text) |  | ||||||
| 			case config.EventNoticeIRC: |  | ||||||
| 				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.i.Cmd.Message(msg.Channel, username+msg.Text) |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -281,18 +239,6 @@ func (b *Birc) getClient() (*girc.Client, error) { | |||||||
| 		user = user[1:] | 		user = user[1:] | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	debug := ioutil.Discard |  | ||||||
| 	if b.GetInt("DebugLevel") == 2 { |  | ||||||
| 		debug = b.Log.Writer() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	pingDelay, err := time.ParseDuration(b.GetString("pingdelay")) |  | ||||||
| 	if err != nil || pingDelay == 0 { |  | ||||||
| 		pingDelay = time.Minute |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	b.Log.Debugf("setting pingdelay to %s", pingDelay) |  | ||||||
|  |  | ||||||
| 	i := girc.New(girc.Config{ | 	i := girc.New(girc.Config{ | ||||||
| 		Server:     server, | 		Server:     server, | ||||||
| 		ServerPass: b.GetString("Password"), | 		ServerPass: b.GetString("Password"), | ||||||
| @@ -302,11 +248,7 @@ func (b *Birc) getClient() (*girc.Client, error) { | |||||||
| 		Name:       b.GetString("Nick"), | 		Name:       b.GetString("Nick"), | ||||||
| 		SSL:        b.GetBool("UseTLS"), | 		SSL:        b.GetBool("UseTLS"), | ||||||
| 		TLSConfig:  &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec | 		TLSConfig:  &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec | ||||||
| 		PingDelay:  pingDelay, | 		PingDelay:  time.Minute, | ||||||
| 		// skip gIRC internal rate limiting, since we have our own throttling |  | ||||||
| 		AllowFlood:    true, |  | ||||||
| 		Debug:         debug, |  | ||||||
| 		SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil}, |  | ||||||
| 	}) | 	}) | ||||||
| 	return i, nil | 	return i, nil | ||||||
| } | } | ||||||
| @@ -332,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 | ||||||
| @@ -343,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 | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -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 |  | ||||||
| } |  | ||||||
| @@ -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 |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -3,72 +3,31 @@ package bmatrix | |||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"html" | ||||||
| 	"mime" | 	"mime" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	matrix "github.com/matrix-org/gomatrix" | 	matrix "github.com/matterbridge/gomatrix" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	htmlTag            = regexp.MustCompile("</.*?>") |  | ||||||
| 	htmlReplacementTag = regexp.MustCompile("<[^>]*>") |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type NicknameCacheEntry struct { |  | ||||||
| 	displayName string |  | ||||||
| 	lastUpdated time.Time |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type Bmatrix struct { | type Bmatrix struct { | ||||||
| 	mc          *matrix.Client | 	mc      *matrix.Client | ||||||
| 	UserID      string | 	UserID  string | ||||||
| 	NicknameMap map[string]NicknameCacheEntry | 	RoomMap map[string]string | ||||||
| 	RoomMap     map[string]string |  | ||||||
| 	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 |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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 | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -80,10 +39,9 @@ func (b *Bmatrix) Connect() error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	resp, err := b.mc.Login(&matrix.ReqLogin{ | 	resp, err := b.mc.Login(&matrix.ReqLogin{ | ||||||
| 		Type:       "m.login.password", | 		Type:     "m.login.password", | ||||||
| 		User:       b.GetString("Login"), | 		User:     b.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 | ||||||
| @@ -100,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.RoomMap[resp.RoomID] = channel.Name | ||||||
| 		b.Lock() | 	b.Unlock() | ||||||
| 		b.RoomMap[resp.RoomID] = channel.Name | 	return err | ||||||
| 		b.Unlock() |  | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bmatrix) Send(msg config.Message) (string, error) { | func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||||
| @@ -120,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, |  | ||||||
| 		} | 		} | ||||||
|  | 		resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m) | ||||||
| 		msgID := "" | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
| 		err := b.retry(func() error { | 		} | ||||||
| 			resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m) | 		return resp.EventID, err | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			msgID = resp.EventID |  | ||||||
|  |  | ||||||
| 			return err |  | ||||||
| 		}) |  | ||||||
|  |  | ||||||
| 		return msgID, err |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Delete message | 	// Delete message | ||||||
| @@ -151,34 +92,17 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | |||||||
| 		if msg.ID == "" { | 		if msg.ID == "" { | ||||||
| 			return "", nil | 			return "", nil | ||||||
| 		} | 		} | ||||||
|  | 		resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{}) | ||||||
| 		msgID := "" | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
| 		err := b.retry(func() error { | 		} | ||||||
| 			resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{}) | 		return resp.EventID, err | ||||||
| 			if err != nil { |  | ||||||
| 				return 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) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -189,105 +113,45 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Edit message if we have an ID | 	// Edit message if we have an ID | ||||||
| 	if msg.ID != "" { | 	// matrix has no editing support | ||||||
| 		rmsg := EditedMessage{TextMessage: matrix.TextMessage{ |  | ||||||
| 			Body:    username.plain + msg.Text, |  | ||||||
| 			MsgType: "m.text", |  | ||||||
| 		}} |  | ||||||
| 		if b.GetBool("HTMLDisable") { |  | ||||||
| 			rmsg.TextMessage.FormattedBody = username.formatted + "* " + msg.Text |  | ||||||
| 		} else { |  | ||||||
| 			rmsg.Format = "org.matrix.custom.html" |  | ||||||
| 			rmsg.TextMessage.FormattedBody = username.formatted + "* " + helper.ParseMarkdown(msg.Text) |  | ||||||
| 		} |  | ||||||
| 		rmsg.NewContent = SubTextMessage{ |  | ||||||
| 			Body:    rmsg.TextMessage.Body, |  | ||||||
| 			MsgType: "m.text", |  | ||||||
| 		} |  | ||||||
| 		rmsg.RelatedTo = MessageRelation{ |  | ||||||
| 			EventID: msg.ID, |  | ||||||
| 			Type:    "m.replace", |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		err := b.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 |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// 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 err := b.mc.Sync(); err != nil { | 			if err := b.mc.Sync(); err != nil { | ||||||
| @@ -297,45 +161,6 @@ func (b *Bmatrix) handlematrix() { | |||||||
| 	}() | 	}() | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool { |  | ||||||
| 	relationInterface, present := ev.Content["m.relates_to"] |  | ||||||
| 	newContentInterface, present2 := ev.Content["m.new_content"] |  | ||||||
| 	if !(present && present2) { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var relation MessageRelation |  | ||||||
| 	if err := interface2Struct(relationInterface, &relation); err != nil { |  | ||||||
| 		b.Log.Warnf("Couldn't parse 'm.relates_to' object with value %#v", relationInterface) |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var newContent SubTextMessage |  | ||||||
| 	if err := interface2Struct(newContentInterface, &newContent); err != nil { |  | ||||||
| 		b.Log.Warnf("Couldn't parse 'm.new_content' object with value %#v", newContentInterface) |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if relation.Type != "m.replace" { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	rmsg.ID = relation.EventID |  | ||||||
| 	rmsg.Text = newContent.Body |  | ||||||
| 	b.Remote <- rmsg |  | ||||||
|  |  | ||||||
| 	return true |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmatrix) handleMemberChange(ev *matrix.Event) { |  | ||||||
| 	// Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information |  | ||||||
| 	if ev.Content["membership"] == "join" { |  | ||||||
| 		if dn, ok := ev.Content["displayname"].(string); ok { |  | ||||||
| 			b.cacheDisplayName(ev.Sender, dn) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmatrix) handleEvent(ev *matrix.Event) { | func (b *Bmatrix) handleEvent(ev *matrix.Event) { | ||||||
| 	b.Log.Debugf("== Receiving event: %#v", ev) | 	b.Log.Debugf("== Receiving event: %#v", ev) | ||||||
| 	if ev.Sender != b.UserID { | 	if ev.Sender != b.UserID { | ||||||
| @@ -347,15 +172,10 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// TODO download avatar | ||||||
|  |  | ||||||
| 		// Create our message | 		// Create our message | ||||||
| 		rmsg := config.Message{ | 		rmsg := config.Message{Username: ev.Sender[1:], Channel: channel, Account: b.Account, UserID: ev.Sender, ID: ev.ID} | ||||||
| 			Username: b.getDisplayName(ev.Sender), |  | ||||||
| 			Channel:  channel, |  | ||||||
| 			Account:  b.Account, |  | ||||||
| 			UserID:   ev.Sender, |  | ||||||
| 			ID:       ev.ID, |  | ||||||
| 			Avatar:   b.getAvatarURL(ev.Sender), |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Text must be a string | 		// Text must be a string | ||||||
| 		if rmsg.Text, ok = ev.Content["body"].(string); !ok { | 		if rmsg.Text, ok = ev.Content["body"].(string); !ok { | ||||||
| @@ -384,11 +204,6 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { | |||||||
| 			rmsg.Event = config.EventUserAction | 			rmsg.Event = config.EventUserAction | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Is it an edit? |  | ||||||
| 		if b.handleEdit(ev, rmsg) { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Do we have attachments | 		// Do we have attachments | ||||||
| 		if b.containsAttachment(ev.Content) { | 		if b.containsAttachment(ev.Content) { | ||||||
| 			err := b.handleDownloadFile(&rmsg, ev.Content) | 			err := b.handleDownloadFile(&rmsg, ev.Content) | ||||||
| @@ -399,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()) |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -478,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 | ||||||
| @@ -510,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 | ||||||
|  | } | ||||||
|   | |||||||
| @@ -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, "<MyUser>", uut.formatted) |  | ||||||
| 	assert.Equal(t, "<MyUser>", uut.plain) |  | ||||||
| } |  | ||||||
| @@ -4,7 +4,7 @@ import ( | |||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/42wim/matterbridge/matterclient" | 	"github.com/42wim/matterbridge/matterclient" | ||||||
| 	"github.com/mattermost/mattermost-server/v5/model" | 	"github.com/mattermost/mattermost-server/model" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // handleDownloadAvatar downloads the avatar of userid from channel | // handleDownloadAvatar downloads the avatar of userid from channel | ||||||
| @@ -66,10 +66,6 @@ func (b *Bmattermost) handleMatter() { | |||||||
| 		} else { | 		} else { | ||||||
| 			b.Log.Debugf("Choosing login/password based receiving") | 			b.Log.Debugf("Choosing login/password based receiving") | ||||||
| 		} | 		} | ||||||
| 		// if for some reason we only want to sent stuff to mattermost but not receive, return |  | ||||||
| 		if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") != "" && b.GetString("Token") == "" && b.GetString("Login") == "" { |  | ||||||
| 			b.Log.Debugf("No WebhookBindAddress specified, only WebhookURL. You will not receive messages from mattermost, only sending is possible.") |  | ||||||
| 		} |  | ||||||
| 		go b.handleMatterClient(messages) | 		go b.handleMatterClient(messages) | ||||||
| 	} | 	} | ||||||
| 	var ok bool | 	var ok bool | ||||||
| @@ -108,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{}), | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import ( | |||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/42wim/matterbridge/matterclient" | 	"github.com/42wim/matterbridge/matterclient" | ||||||
| 	"github.com/42wim/matterbridge/matterhook" | 	"github.com/42wim/matterbridge/matterhook" | ||||||
| 	"github.com/mattermost/mattermost-server/v5/model" | 	"github.com/mattermost/mattermost-server/model" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (b *Bmattermost) doConnectWebhookBind() error { | func (b *Bmattermost) doConnectWebhookBind() error { | ||||||
| @@ -70,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() | ||||||
|   | |||||||
| @@ -122,20 +122,11 @@ func (b *Bmattermost) Send(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) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// we only can reply to the root of the thread, not to a specific ID (like discord for example does) |  | ||||||
| 	if msg.ParentID != "" { |  | ||||||
| 		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) |  | ||||||
| 		} |  | ||||||
| 		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) { | ||||||
|   | |||||||
| @@ -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" |  | ||||||
| } |  | ||||||
| @@ -1,227 +0,0 @@ | |||||||
| package bmsteams |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"regexp" |  | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" |  | ||||||
| 	"github.com/davecgh/go-spew/spew" |  | ||||||
|  |  | ||||||
| 	"github.com/mattn/godown" |  | ||||||
| 	msgraph "github.com/yaegashi/msgraph.go/beta" |  | ||||||
| 	"github.com/yaegashi/msgraph.go/msauth" |  | ||||||
|  |  | ||||||
| 	"golang.org/x/oauth2" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"} |  | ||||||
| var attachRE = regexp.MustCompile(`<attachment id=.*?attachment>`) |  | ||||||
|  |  | ||||||
| type Bmsteams struct { |  | ||||||
| 	gc    *msgraph.GraphServiceRequestBuilder |  | ||||||
| 	ctx   context.Context |  | ||||||
| 	botID string |  | ||||||
| 	*bridge.Config |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { |  | ||||||
| 	return &Bmsteams{Config: cfg} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmsteams) Connect() error { |  | ||||||
| 	tokenCachePath := b.GetString("sessionFile") |  | ||||||
| 	if tokenCachePath == "" { |  | ||||||
| 		tokenCachePath = "msteams_session.json" |  | ||||||
| 	} |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	m := msauth.NewManager() |  | ||||||
| 	m.LoadFile(tokenCachePath) //nolint:errcheck |  | ||||||
| 	ts, err := m.DeviceAuthorizationGrant(ctx, b.GetString("TenantID"), b.GetString("ClientID"), defaultScopes, nil) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	err = m.SaveFile(tokenCachePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err) |  | ||||||
| 	} |  | ||||||
| 	// make file readable only for matterbridge user |  | ||||||
| 	err = os.Chmod(tokenCachePath, 0600) |  | ||||||
| 	if err != nil { |  | ||||||
| 		b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err) |  | ||||||
| 	} |  | ||||||
| 	httpClient := oauth2.NewClient(ctx, ts) |  | ||||||
| 	graphClient := msgraph.NewClient(httpClient) |  | ||||||
| 	b.gc = graphClient |  | ||||||
| 	b.ctx = ctx |  | ||||||
|  |  | ||||||
| 	err = b.setBotID() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	b.Log.Info("Connection succeeded") |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmsteams) Disconnect() error { |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error { |  | ||||||
| 	go func(name string) { |  | ||||||
| 		for { |  | ||||||
| 			err := b.poll(name) |  | ||||||
| 			if err != nil { |  | ||||||
| 				b.Log.Errorf("polling failed for %s: %s. retrying in 5 seconds", name, err) |  | ||||||
| 			} |  | ||||||
| 			time.Sleep(time.Second * 5) |  | ||||||
| 		} |  | ||||||
| 	}(channel.Name) |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmsteams) Send(msg config.Message) (string, error) { |  | ||||||
| 	b.Log.Debugf("=> Receiving %#v", msg) |  | ||||||
| 	if msg.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.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() |  | ||||||
| } |  | ||||||
| @@ -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 |  | ||||||
| } |  | ||||||
| @@ -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 `]+)` 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 |  | ||||||
| } |  | ||||||
| @@ -1,259 +0,0 @@ | |||||||
| package bmumble |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"crypto/tls" |  | ||||||
| 	"crypto/x509" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net" |  | ||||||
| 	"strconv" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"layeh.com/gumble/gumble" |  | ||||||
| 	"layeh.com/gumble/gumbleutil" |  | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" |  | ||||||
| 	stripmd "github.com/writeas/go-strip-markdown" |  | ||||||
|  |  | ||||||
| 	// We need to import the 'data' package as an implicit dependency. |  | ||||||
| 	// See: https://godoc.org/github.com/paulrosania/go-charset/charset |  | ||||||
| 	_ "github.com/paulrosania/go-charset/data" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type Bmumble struct { |  | ||||||
| 	client             *gumble.Client |  | ||||||
| 	Nick               string |  | ||||||
| 	Host               string |  | ||||||
| 	Channel            *uint32 |  | ||||||
| 	local              chan config.Message |  | ||||||
| 	running            chan error |  | ||||||
| 	connected          chan gumble.DisconnectEvent |  | ||||||
| 	serverConfigUpdate chan gumble.ServerConfigEvent |  | ||||||
| 	serverConfig       gumble.ServerConfigEvent |  | ||||||
| 	tlsConfig          tls.Config |  | ||||||
|  |  | ||||||
| 	*bridge.Config |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { |  | ||||||
| 	b := &Bmumble{} |  | ||||||
| 	b.Config = cfg |  | ||||||
| 	b.Nick = b.GetString("Nick") |  | ||||||
| 	b.local = make(chan config.Message) |  | ||||||
| 	b.running = make(chan error) |  | ||||||
| 	b.connected = make(chan gumble.DisconnectEvent) |  | ||||||
| 	b.serverConfigUpdate = make(chan gumble.ServerConfigEvent) |  | ||||||
| 	return b |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmumble) Connect() error { |  | ||||||
| 	b.Log.Infof("Connecting %s", b.GetString("Server")) |  | ||||||
| 	host, portstr, err := net.SplitHostPort(b.GetString("Server")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	b.Host = host |  | ||||||
| 	_, err = strconv.Atoi(portstr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err = b.buildTLSConfig(); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	go b.doSend() |  | ||||||
| 	go b.connectLoop() |  | ||||||
| 	err = <-b.running |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmumble) Disconnect() error { |  | ||||||
| 	return b.client.Disconnect() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error { |  | ||||||
| 	cid, err := strconv.ParseUint(channel.Name, 10, 32) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	channelID := uint32(cid) |  | ||||||
| 	if b.Channel != nil && *b.Channel != channelID { |  | ||||||
| 		b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel) |  | ||||||
| 		return errors.New("the Mumble bridge can only join a single channel") |  | ||||||
| 	} |  | ||||||
| 	b.Channel = &channelID |  | ||||||
| 	return b.doJoin(b.client, channelID) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmumble) Send(msg config.Message) (string, error) { |  | ||||||
| 	// Only process text messages |  | ||||||
| 	b.Log.Debugf("=> Received local message %#v", msg) |  | ||||||
| 	if msg.Event != "" && msg.Event != config.EventUserAction { |  | ||||||
| 		return "", nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	attachments := b.extractFiles(&msg) |  | ||||||
| 	b.local <- msg |  | ||||||
| 	for _, a := range attachments { |  | ||||||
| 		b.local <- a |  | ||||||
| 	} |  | ||||||
| 	return "", nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmumble) buildTLSConfig() error { |  | ||||||
| 	b.tlsConfig = tls.Config{} |  | ||||||
| 	// Load TLS client certificate keypair required for registered user authentication |  | ||||||
| 	if cpath := b.GetString("TLSClientCertificate"); cpath != "" { |  | ||||||
| 		if ckey := b.GetString("TLSClientKey"); ckey != "" { |  | ||||||
| 			cert, err := tls.LoadX509KeyPair(cpath, ckey) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			b.tlsConfig.Certificates = []tls.Certificate{cert} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	// Load TLS CA used for server verification.  If not provided, the Go system trust anchor is used |  | ||||||
| 	if capath := b.GetString("TLSCACertificate"); capath != "" { |  | ||||||
| 		ca, err := ioutil.ReadFile(capath) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		b.tlsConfig.RootCAs = x509.NewCertPool() |  | ||||||
| 		b.tlsConfig.RootCAs.AppendCertsFromPEM(ca) |  | ||||||
| 	} |  | ||||||
| 	b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify") |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmumble) connectLoop() { |  | ||||||
| 	firstConnect := true |  | ||||||
| 	for { |  | ||||||
| 		err := b.doConnect() |  | ||||||
| 		if firstConnect { |  | ||||||
| 			b.running <- err |  | ||||||
| 		} |  | ||||||
| 		if err != nil { |  | ||||||
| 			b.Log.Errorf("Connection to server failed: %#v", err) |  | ||||||
| 			if firstConnect { |  | ||||||
| 				break |  | ||||||
| 			} else { |  | ||||||
| 				b.Log.Info("Retrying in 10s") |  | ||||||
| 				time.Sleep(10 * time.Second) |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		firstConnect = false |  | ||||||
| 		d := <-b.connected |  | ||||||
| 		switch d.Type { |  | ||||||
| 		case gumble.DisconnectError: |  | ||||||
| 			b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String) |  | ||||||
| 			continue |  | ||||||
| 		case gumble.DisconnectKicked: |  | ||||||
| 			b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String) |  | ||||||
| 			continue |  | ||||||
| 		case gumble.DisconnectBanned: |  | ||||||
| 			b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String) |  | ||||||
| 			close(b.connected) |  | ||||||
| 			close(b.running) |  | ||||||
| 			return |  | ||||||
| 		case gumble.DisconnectUser: |  | ||||||
| 			b.Log.Infof("Disconnect successful") |  | ||||||
| 			close(b.connected) |  | ||||||
| 			close(b.running) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmumble) doConnect() error { |  | ||||||
| 	// Create new gumble config and attach event handlers |  | ||||||
| 	gumbleConfig := gumble.NewConfig() |  | ||||||
| 	gumbleConfig.Attach(gumbleutil.Listener{ |  | ||||||
| 		ServerConfig: b.handleServerConfig, |  | ||||||
| 		TextMessage:  b.handleTextMessage, |  | ||||||
| 		Connect:      b.handleConnect, |  | ||||||
| 		Disconnect:   b.handleDisconnect, |  | ||||||
| 		UserChange:   b.handleUserChange, |  | ||||||
| 	}) |  | ||||||
| 	gumbleConfig.Username = b.GetString("Nick") |  | ||||||
| 	if password := b.GetString("Password"); password != "" { |  | ||||||
| 		gumbleConfig.Password = password |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	b.client = client |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error { |  | ||||||
| 	channel, ok := client.Channels[channelID] |  | ||||||
| 	if !ok { |  | ||||||
| 		return fmt.Errorf("no channel with ID %d", channelID) |  | ||||||
| 	} |  | ||||||
| 	client.Self.Move(channel) |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmumble) doSend() { |  | ||||||
| 	// Message sending loop that makes sure server-side |  | ||||||
| 	// restrictions and client-side message traits don't conflict |  | ||||||
| 	// with each other. |  | ||||||
| 	for { |  | ||||||
| 		select { |  | ||||||
| 		case serverConfig := <-b.serverConfigUpdate: |  | ||||||
| 			b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength) |  | ||||||
| 			b.serverConfig = serverConfig |  | ||||||
| 		case msg := <-b.local: |  | ||||||
| 			b.processMessage(&msg) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmumble) processMessage(msg *config.Message) { |  | ||||||
| 	b.Log.Debugf("Processing message %s", msg.Text) |  | ||||||
|  |  | ||||||
| 	allowHTML := true |  | ||||||
| 	if b.serverConfig.AllowHTML != nil { |  | ||||||
| 		allowHTML = *b.serverConfig.AllowHTML |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// If this is a specially generated image message, send it unmodified |  | ||||||
| 	if msg.Event == "mumble_image" { |  | ||||||
| 		if allowHTML { |  | ||||||
| 			b.client.Self.Channel.Send(msg.Username+msg.Text, false) |  | ||||||
| 		} else { |  | ||||||
| 			b.Log.Info("Can't send image, server does not allow HTML messages") |  | ||||||
| 		} |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Don't process empty messages |  | ||||||
| 	if len(msg.Text) == 0 { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	// If HTML is allowed, convert markdown into HTML, otherwise strip markdown |  | ||||||
| 	if allowHTML { |  | ||||||
| 		msg.Text = helper.ParseMarkdown(msg.Text) |  | ||||||
| 	} else { |  | ||||||
| 		msg.Text = stripmd.Strip(msg.Text) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// If there is a maximum message length, split and truncate the lines |  | ||||||
| 	var msgLines []string |  | ||||||
| 	if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil { |  | ||||||
| 		msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username)) |  | ||||||
| 	} else { |  | ||||||
| 		msgLines = helper.GetSubLines(msg.Text, 0) |  | ||||||
| 	} |  | ||||||
| 	// Send the individual lindes |  | ||||||
| 	for i := range msgLines { |  | ||||||
| 		b.client.Self.Channel.Send(msg.Username+msgLines[i], false) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,210 +0,0 @@ | |||||||
| package nctalk |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"crypto/tls" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" |  | ||||||
|  |  | ||||||
| 	"gomod.garykim.dev/nc-talk/ocs" |  | ||||||
| 	"gomod.garykim.dev/nc-talk/room" |  | ||||||
| 	"gomod.garykim.dev/nc-talk/user" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type Btalk struct { |  | ||||||
| 	user  *user.TalkUser |  | ||||||
| 	rooms []Broom |  | ||||||
| 	*bridge.Config |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { |  | ||||||
| 	return &Btalk{Config: cfg} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type Broom struct { |  | ||||||
| 	room      *room.TalkRoom |  | ||||||
| 	ctx       context.Context |  | ||||||
| 	ctxCancel context.CancelFunc |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Btalk) Connect() error { |  | ||||||
| 	b.Log.Info("Connecting") |  | ||||||
| 	tconfig := &user.TalkUserConfig{ |  | ||||||
| 		TLSConfig: &tls.Config{ |  | ||||||
| 			InsecureSkipVerify: b.GetBool("SkipTLSVerify"), //nolint:gosec |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	var err error |  | ||||||
| 	b.user, err = user.NewUser(b.GetString("Server"), b.GetString("Login"), b.GetString("Password"), tconfig) |  | ||||||
| 	if err != nil { |  | ||||||
| 		b.Log.Error("Config could not be used") |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	_, err = b.user.Capabilities() |  | ||||||
| 	if err != nil { |  | ||||||
| 		b.Log.Error("Cannot Connect") |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	b.Log.Info("Connected") |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Btalk) Disconnect() error { |  | ||||||
| 	for _, r := range b.rooms { |  | ||||||
| 		r.ctxCancel() |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Btalk) JoinChannel(channel config.ChannelInfo) error { |  | ||||||
| 	tr, err := room.NewTalkRoom(b.user, channel.Name) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	newRoom := Broom{ |  | ||||||
| 		room: tr, |  | ||||||
| 	} |  | ||||||
| 	newRoom.ctx, newRoom.ctxCancel = context.WithCancel(context.Background()) |  | ||||||
| 	c, err := newRoom.room.ReceiveMessages(newRoom.ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	b.rooms = append(b.rooms, newRoom) |  | ||||||
|  |  | ||||||
| 	// Config |  | ||||||
| 	guestSuffix := " (Guest)" |  | ||||||
| 	if b.IsKeySet("GuestSuffix") { |  | ||||||
| 		guestSuffix = b.GetString("GuestSuffix") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	go func() { |  | ||||||
| 		for msg := range c { |  | ||||||
| 			msg := msg |  | ||||||
|  |  | ||||||
| 			if msg.Error != nil { |  | ||||||
| 				b.Log.Errorf("Fatal message poll error: %s\n", msg.Error) |  | ||||||
|  |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// ignore messages that are one of the following |  | ||||||
| 			// * not a message from a user |  | ||||||
| 			// * from ourselves |  | ||||||
| 			if msg.MessageType != ocs.MessageComment || msg.ActorID == b.user.User { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			remoteMessage := config.Message{ |  | ||||||
| 				Text:     formatRichObjectString(msg.Message, msg.MessageParameters), |  | ||||||
| 				Channel:  newRoom.room.Token, |  | ||||||
| 				Username: DisplayName(msg, guestSuffix), |  | ||||||
| 				UserID:   msg.ActorID, |  | ||||||
| 				Account:  b.Account, |  | ||||||
| 			} |  | ||||||
| 			// It is possible for the ID to not be set on older versions of Talk so we only set it if |  | ||||||
| 			// the ID is not blank |  | ||||||
| 			if msg.ID != 0 { |  | ||||||
| 				remoteMessage.ID = strconv.Itoa(msg.ID) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// Handle Files |  | ||||||
| 			err = b.handleFiles(&remoteMessage, &msg) |  | ||||||
| 			if err != nil { |  | ||||||
| 				b.Log.Errorf("Error handling file: %#v", msg) |  | ||||||
|  |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			b.Log.Debugf("<= Message is %#v", remoteMessage) |  | ||||||
| 			b.Remote <- remoteMessage |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Btalk) Send(msg config.Message) (string, error) { |  | ||||||
| 	r := b.getRoom(msg.Channel) |  | ||||||
| 	if r == nil { |  | ||||||
| 		b.Log.Errorf("Could not find room for %v", msg.Channel) |  | ||||||
| 		return "", nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Talk currently only supports sending normal messages |  | ||||||
| 	if msg.Event != "" { |  | ||||||
| 		return "", nil |  | ||||||
| 	} |  | ||||||
| 	sentMessage, err := r.room.SendMessage(msg.Username + msg.Text) |  | ||||||
| 	if err != nil { |  | ||||||
| 		b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err) |  | ||||||
| 		return "", nil |  | ||||||
| 	} |  | ||||||
| 	return strconv.Itoa(sentMessage.ID), nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Btalk) getRoom(token string) *Broom { |  | ||||||
| 	for _, r := range b.rooms { |  | ||||||
| 		if r.room.Token == token { |  | ||||||
| 			return &r |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error { |  | ||||||
| 	for _, parameter := range message.MessageParameters { |  | ||||||
| 		if parameter.Type == ocs.ROSTypeFile { |  | ||||||
| 			// Get the file |  | ||||||
| 			file, err := b.user.DownloadFile(parameter.Path) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if mmsg.Extra == nil { |  | ||||||
| 				mmsg.Extra = make(map[string][]interface{}) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			mmsg.Extra["file"] = append(mmsg.Extra["file"], config.FileInfo{ |  | ||||||
| 				Name:   parameter.Name, |  | ||||||
| 				Data:   file, |  | ||||||
| 				Size:   int64(len(*file)), |  | ||||||
| 				Avatar: false, |  | ||||||
| 			}) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785 |  | ||||||
| func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string { |  | ||||||
| 	for id, parameter := range parameters { |  | ||||||
| 		text := parameter.Name |  | ||||||
|  |  | ||||||
| 		switch parameter.Type { |  | ||||||
| 		case ocs.ROSTypeUser, ocs.ROSTypeGroup: |  | ||||||
| 			text = "@" + text |  | ||||||
| 		case ocs.ROSTypeFile: |  | ||||||
| 			if parameter.Link != "" { |  | ||||||
| 				text = parameter.Name |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		message = strings.ReplaceAll(message, "{"+id+"}", text) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return message |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func DisplayName(msg ocs.TalkRoomMessageData, suffix string) string { |  | ||||||
| 	if msg.ActorType == ocs.ActorGuest { |  | ||||||
| 		if msg.ActorDisplayName == "" { |  | ||||||
| 			return "Guest" |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return msg.ActorDisplayName + suffix |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return msg.ActorDisplayName |  | ||||||
| } |  | ||||||
| @@ -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,51 +58,11 @@ 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"] { | ||||||
| 		fi := f.(config.FileInfo) | 		fi := f.(config.FileInfo) | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 == "" { | ||||||
|   | |||||||
| @@ -1,22 +1,18 @@ | |||||||
| package bslack | package bslack | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html" | 	"html" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/slack-go/slack" | 	"github.com/nlopes/slack" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // ErrEventIgnored is for events that should be ignored |  | ||||||
| var ErrEventIgnored = errors.New("this event message should ignored") |  | ||||||
|  |  | ||||||
| func (b *Bslack) handleSlack() { | func (b *Bslack) handleSlack() { | ||||||
| 	messages := make(chan *config.Message) | 	messages := make(chan *config.Message) | ||||||
| 	if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" { | 	if b.GetString(incomingWebhookConfig) != "" { | ||||||
| 		b.Log.Debugf("Choosing webhooks based receiving") | 		b.Log.Debugf("Choosing webhooks based receiving") | ||||||
| 		go b.handleMatterHook(messages) | 		go b.handleMatterHook(messages) | ||||||
| 	} else { | 	} else { | ||||||
| @@ -34,7 +30,6 @@ func (b *Bslack) handleSlack() { | |||||||
| 			message.Text = b.replaceVariable(message.Text) | 			message.Text = b.replaceVariable(message.Text) | ||||||
| 			message.Text = b.replaceChannel(message.Text) | 			message.Text = b.replaceChannel(message.Text) | ||||||
| 			message.Text = b.replaceURL(message.Text) | 			message.Text = b.replaceURL(message.Text) | ||||||
| 			message.Text = b.replaceb0rkedMarkDown(message.Text) |  | ||||||
| 			message.Text = html.UnescapeString(message.Text) | 			message.Text = html.UnescapeString(message.Text) | ||||||
|  |  | ||||||
| 			// Add the avatar | 			// Add the avatar | ||||||
| @@ -48,7 +43,7 @@ func (b *Bslack) handleSlack() { | |||||||
|  |  | ||||||
| func (b *Bslack) handleSlackClient(messages chan *config.Message) { | func (b *Bslack) handleSlackClient(messages chan *config.Message) { | ||||||
| 	for msg := range b.rtm.IncomingEvents { | 	for msg := range b.rtm.IncomingEvents { | ||||||
| 		if msg.Type != sUserTyping && msg.Type != sHello && msg.Type != sLatencyReport { | 		if msg.Type != sUserTyping && msg.Type != sLatencyReport { | ||||||
| 			b.Log.Debugf("== Receiving event %#v", msg.Data) | 			b.Log.Debugf("== Receiving event %#v", msg.Data) | ||||||
| 		} | 		} | ||||||
| 		switch ev := msg.Data.(type) { | 		switch ev := msg.Data.(type) { | ||||||
| @@ -57,9 +52,7 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) { | |||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 			rmsg, err := b.handleTypingEvent(ev) | 			rmsg, err := b.handleTypingEvent(ev) | ||||||
| 			if err == ErrEventIgnored { | 			if err != nil { | ||||||
| 				continue |  | ||||||
| 			} else if err != nil { |  | ||||||
| 				b.Log.Errorf("%#v", err) | 				b.Log.Errorf("%#v", err) | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| @@ -93,7 +86,7 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) { | |||||||
| 			b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) | 			b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) | ||||||
| 		case *slack.MemberJoinedChannelEvent: | 		case *slack.MemberJoinedChannelEvent: | ||||||
| 			b.users.populateUser(ev.User) | 			b.users.populateUser(ev.User) | ||||||
| 		case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent: | 		case *slack.LatencyReport: | ||||||
| 			continue | 			continue | ||||||
| 		default: | 		default: | ||||||
| 			b.Log.Debugf("Unhandled incoming event: %T", ev) | 			b.Log.Debugf("Unhandled incoming event: %T", ev) | ||||||
| @@ -130,11 +123,11 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check for our callback ID | 	// Skip any messages that we made ourselves or from 'slackbot' (see #527). | ||||||
| 	hasOurCallbackID := false | 	if ev.Username == sSlackBotUser || | ||||||
| 	if len(ev.Blocks.BlockSet) == 1 { | 		(b.rtm != nil && ev.Username == b.si.User.Name) || | ||||||
| 		block, ok := ev.Blocks.BlockSet[0].(*slack.SectionBlock) | 		(len(ev.Attachments) > 0 && ev.Attachments[0].CallbackID == "matterbridge_"+b.uuid) { | ||||||
| 		hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid | 		return true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if ev.SubMessage != nil { | 	if ev.SubMessage != nil { | ||||||
| @@ -149,16 +142,6 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { | |||||||
| 		if ev.SubType == "message_replied" && ev.Hidden { | 		if ev.SubType == "message_replied" && ev.Hidden { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| 		if len(ev.SubMessage.Blocks.BlockSet) == 1 { |  | ||||||
| 			block, ok := ev.SubMessage.Blocks.BlockSet[0].(*slack.SectionBlock) |  | ||||||
| 			hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Skip any messages that we made ourselves or from 'slackbot' (see #527). |  | ||||||
| 	if ev.Username == sSlackBotUser || |  | ||||||
| 		(b.rtm != nil && ev.Username == b.si.User.Name) || hasOurCallbackID { |  | ||||||
| 		return true |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(ev.Files) > 0 { | 	if len(ev.Files) > 0 { | ||||||
| @@ -286,9 +269,6 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) | |||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) { | func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) { | ||||||
| 	if ev.User == b.si.User.ID { |  | ||||||
| 		return nil, ErrEventIgnored |  | ||||||
| 	} |  | ||||||
| 	channelInfo, err := b.channels.getChannelByID(ev.Channel) | 	channelInfo, err := b.channels.getChannelByID(ev.Channel) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|   | |||||||
| @@ -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 > |  | ||||||
| 		{ |  | ||||||
| 			regexp.MustCompile(`(?sm)^>`), |  | ||||||
| 			">", |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	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, "```") | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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.JoinChannel(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) |  | ||||||
| 	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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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" | 	"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,32 +38,22 @@ 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 | 		usernameForward := "" | ||||||
| 	} | 		if b.GetBool("UseFirstName") { | ||||||
|  |  | ||||||
| 	if message.ForwardFrom == nil { |  | ||||||
| 		rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	usernameForward := "" |  | ||||||
| 	if b.GetBool("UseFirstName") { |  | ||||||
| 		usernameForward = message.ForwardFrom.FirstName |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if usernameForward == "" { |  | ||||||
| 		usernameForward = message.ForwardFrom.UserName |  | ||||||
| 		if usernameForward == "" { |  | ||||||
| 			usernameForward = message.ForwardFrom.FirstName | 			usernameForward = message.ForwardFrom.FirstName | ||||||
| 		} | 		} | ||||||
|  | 		if usernameForward == "" { | ||||||
|  | 			usernameForward = message.ForwardFrom.UserName | ||||||
|  | 			if usernameForward == "" { | ||||||
|  | 				usernameForward = message.ForwardFrom.FirstName | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if usernameForward == "" { | ||||||
|  | 			usernameForward = unknownUser | ||||||
|  | 		} | ||||||
|  | 		rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if usernameForward == "" { |  | ||||||
| 		usernameForward = unknownUser |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // handleQuoting handles quoting of previous messages | // handleQuoting handles quoting of previous messages | ||||||
| @@ -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) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -181,15 +170,13 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { | |||||||
| // 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 int, 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.Itoa(userid), | ||||||
| 		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.Itoa(userid)]; !ok { | 	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}) | ||||||
| @@ -219,46 +206,6 @@ func (b *Btelegram) handleDownloadAvatar(userid int, channel string) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) { |  | ||||||
| 	var format string |  | ||||||
| 	switch b.GetString("MediaConvertTgs") { |  | ||||||
| 	case FormatWebp: |  | ||||||
| 		b.Log.Debugf("Tgs to WebP conversion enabled, converting %v", name) |  | ||||||
| 		format = FormatWebp |  | ||||||
| 	case FormatPng: |  | ||||||
| 		// The WebP to PNG converter can't handle animated webp files yet, |  | ||||||
| 		// and I'm not going to write a path for x/image/webp. |  | ||||||
| 		// The error message would be: |  | ||||||
| 		//     conversion failed: webp: non-Alpha VP8X is not implemented |  | ||||||
| 		// So instead, we tell lottie to directly go to PNG. |  | ||||||
| 		b.Log.Debugf("Tgs to PNG conversion enabled, converting %v", name) |  | ||||||
| 		format = FormatPng |  | ||||||
| 	default: |  | ||||||
| 		// Otherwise, no conversion was requested. Trying to run the usual webp |  | ||||||
| 		// converter would fail, because '.tgs.webp' is actually a gzipped JSON |  | ||||||
| 		// file, and has nothing to do with WebP. |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	err := helper.ConvertTgsToX(data, format, b.Log) |  | ||||||
| 	if err != nil { |  | ||||||
| 		b.Log.Errorf("conversion failed: %v", err) |  | ||||||
| 	} else { |  | ||||||
| 		*name = strings.Replace(*name, "tgs.webp", format, 1) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) { |  | ||||||
| 	if b.GetBool("MediaConvertWebPToPNG") { |  | ||||||
| 		b.Log.Debugf("WebP to PNG conversion enabled, converting %v", name) |  | ||||||
| 		err := helper.ConvertWebPToPNG(data) |  | ||||||
| 		if err != nil { |  | ||||||
| 			b.Log.Errorf("conversion failed: %v", err) |  | ||||||
| 		} else { |  | ||||||
| 			*name = strings.Replace(*name, ".webp", ".png", 1) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // handleDownloadFile handles file download | // handleDownloadFile handles file download | ||||||
| func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error { | func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error { | ||||||
| 	size := 0 | 	size := 0 | ||||||
| @@ -306,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 | ||||||
| } | } | ||||||
| @@ -367,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") | ||||||
| @@ -391,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.NewPhotoUpload(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.NewVideoUpload(chatid, file) |  | ||||||
| 			vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) |  | ||||||
| 			c = vc |  | ||||||
| 		case ".mp3", ".oga": |  | ||||||
| 			ac := tgbotapi.NewAudioUpload(chatid, file) |  | ||||||
| 			ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) |  | ||||||
| 			c = ac |  | ||||||
| 		case ".ogg": |  | ||||||
| 			voc := tgbotapi.NewVoiceUpload(chatid, file) |  | ||||||
| 			voc.Caption, voc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) |  | ||||||
| 			c = voc |  | ||||||
| 		default: |  | ||||||
| 			dc := tgbotapi.NewDocumentUpload(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 "" | ||||||
| } | } | ||||||
| @@ -426,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) | ||||||
| @@ -453,13 +375,8 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa | |||||||
| 				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) |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,23 +2,19 @@ package btelegram | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"html" | 	"html" | ||||||
| 	"log" |  | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" | 	"github.com/go-telegram-bot-api/telegram-bot-api" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	unknownUser = "unknown" | 	unknownUser = "unknown" | ||||||
| 	HTMLFormat  = "HTML" | 	HTMLFormat  = "HTML" | ||||||
| 	HTMLNick    = "htmlnick" | 	HTMLNick    = "htmlnick" | ||||||
| 	MarkdownV2  = "MarkdownV2" |  | ||||||
| 	FormatPng   = "png" |  | ||||||
| 	FormatWebp  = "webp" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Btelegram struct { | type Btelegram struct { | ||||||
| @@ -28,16 +24,6 @@ type Btelegram struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
| 	tgsConvertFormat := cfg.GetString("MediaConvertTgs") |  | ||||||
| 	if tgsConvertFormat != "" { |  | ||||||
| 		err := helper.CanConvertTgsToX() |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but lottie does not appear to work:\n%#v", tgsConvertFormat, err) |  | ||||||
| 		} |  | ||||||
| 		if tgsConvertFormat != FormatPng && tgsConvertFormat != FormatWebp { |  | ||||||
| 			log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but only '%s' and '%s' are supported.", FormatPng, FormatWebp, tgsConvertFormat) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return &Btelegram{Config: cfg, avatarMap: make(map[string]string)} | 	return &Btelegram{Config: cfg, avatarMap: make(map[string]string)} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -69,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) | ||||||
|  |  | ||||||
| @@ -117,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) | ||||||
| @@ -133,14 +97,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Post normal message | 	// Post normal message | ||||||
| 	// TODO: recheck it. | 	return b.sendMessage(chatid, msg.Username, msg.Text) | ||||||
| 	// 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 "", nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Btelegram) getFileDirectURL(id string) string { | func (b *Btelegram) getFileDirectURL(id string) string { | ||||||
| @@ -153,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 | ||||||
|   | |||||||
							
								
								
									
										327
									
								
								bridge/vk/vk.go
									
									
									
									
									
								
							
							
						
						
									
										327
									
								
								bridge/vk/vk.go
									
									
									
									
									
								
							| @@ -1,327 +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 |  | ||||||
| 	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")) |  | ||||||
| 	lp, err := longpoll.NewLongPoll(b.c, b.GetInt("GroupID")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		b.Log.Debugf("%#v", err) |  | ||||||
|  |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) { |  | ||||||
| 		b.handleMessage(obj.Message, false) |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	b.Log.Info("Connection succeeded") |  | ||||||
|  |  | ||||||
| 	go func() { |  | ||||||
| 		err := lp.Run() |  | ||||||
| 		if err != nil { |  | ||||||
| 			b.Log.Fatal("Enable longpoll in group management") |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bvk) Disconnect() error { |  | ||||||
| 	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) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -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,218 +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, | ||||||
| 		Account:  b.Account, | 		Channel:   groupJid, | ||||||
| 		Protocol: b.Protocol, | 		Account:   b.Account, | ||||||
| 		Extra:    make(map[string][]interface{}), | 		Protocol:  b.Protocol, | ||||||
| 		//	ParentID: TODO, // TODO handle thread replies  // map from Info.QuotedMessageID string | 		Extra:     make(map[string][]interface{}), | ||||||
| 		ID: message.Info.Id, | 		//		ParentID: TODO, // TODO handle thread replies  // map from Info.QuotedMessageID string | ||||||
| 	} | 		//	Event     string    `json:"event"` | ||||||
|  | 		//	Gateway   string  // will be added during message processing | ||||||
|  | 		ID: message.Info.Id} | ||||||
|  |  | ||||||
| 	if avatarURL, exists := b.userAvatars[senderJID]; exists { | 	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" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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 |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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 |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -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") |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
| 	session   *whatsapp.Session | 	// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21 | ||||||
| 	conn      *whatsapp.Conn | 	session *whatsapp.Session | ||||||
|  | 	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 { | ||||||
| 	if err != nil { | 		var session whatsapp.Session | ||||||
| 		b.Log.Warn(err.Error()) | 		session, err = b.readSession() | ||||||
|  |  | ||||||
|  | 		if err == nil { | ||||||
|  | 			b.Log.Debugln("Restoring WhatsApp session..") | ||||||
|  |  | ||||||
|  | 			// https://github.com/Rhymen/go-whatsapp#restore | ||||||
|  | 			session, err = b.conn.RestoreSession(session) | ||||||
|  | 			if err != nil { | ||||||
|  | 				// TODO return or continue to normal login? | ||||||
|  | 				// restore session connection timed out (I couldn't get over it without logging in again) | ||||||
|  | 				return errors.New("failed to restore session: " + err.Error()) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			b.session = &session | ||||||
|  | 			b.Log.Debugln("Session restored successfully!") | ||||||
|  | 		} else { | ||||||
|  | 			b.Log.Warn(err.Error()) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// login to a new session | 	// 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 { | 			} else { | ||||||
| 				b.Lock() | 				// TODO any race conditions here? | ||||||
| 				b.userAvatars[jid] = info.URL | 				b.userAvatars[jid] = info.URL | ||||||
| 				b.Unlock() |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		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,122 +167,74 @@ 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 | ||||||
| 	} | 		var jids []string | ||||||
|  |  | ||||||
| 	// channel.Name specifies group name that might change, warn about it |  | ||||||
| 	var jids []string |  | ||||||
| 	for id, contact := range b.conn.Store.Contacts { |  | ||||||
| 		if isGroupJid(id) && contact.Name == channel.Name { |  | ||||||
| 			jids = append(jids, id) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	switch len(jids) { |  | ||||||
| 	case 0: |  | ||||||
| 		// didn't match any group - print out possibilites |  | ||||||
| 		for id, contact := range b.conn.Store.Contacts { | 		for id, contact := range b.conn.Store.Contacts { | ||||||
| 			if isGroupJid(id) { | 			if isGroupJid(id) && contact.Name == channel.Name { | ||||||
| 				b.Log.Infof("%s %s", contact.Jid, contact.Name) | 				jids = append(jids, id) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name) | 		switch len(jids) { | ||||||
| 	case 1: | 		case 0: | ||||||
| 		return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name) | 			// didn't match any group - print out possibilites | ||||||
| 	default: | 			// TODO sort | ||||||
| 		return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids) | 			// copy b; | ||||||
| 	} | 			//sort.Slice(people, func(i, j int) bool { | ||||||
| } | 			//	return people[i].Age > people[j].Age | ||||||
|  | 			//}) | ||||||
|  | 			for id, contact := range b.conn.Store.Contacts { | ||||||
|  | 				if isGroupJid(id) { | ||||||
|  | 					b.Log.Infof("%s %s", contact.Jid, contact.Name) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name) | ||||||
|  |  | ||||||
| // Post a document message from the bridge to WhatsApp | 		case 1: | ||||||
| func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) { | 			return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name) | ||||||
| 	fi := msg.Extra["file"][0].(config.FileInfo) |  | ||||||
|  |  | ||||||
| 	// Post document message | 		default: | ||||||
| 	message := whatsapp.DocumentMessage{ | 			return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids) | ||||||
| 		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) | 	return nil | ||||||
|  |  | ||||||
| 	// 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 | ||||||
|   | |||||||
| @@ -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 |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -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 "" |  | ||||||
| } |  | ||||||
| @@ -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 { | ||||||
|  | 	xc      *xmpp.Client | ||||||
|  | 	xmppMap map[string]string | ||||||
| 	*bridge.Config | 	*bridge.Config | ||||||
|  |  | ||||||
| 	startTime time.Time | 	startTime time.Time | ||||||
| 	xc        *xmpp.Client |  | ||||||
| 	xmppMap   map[string]string |  | ||||||
| 	connected bool |  | ||||||
| 	sync.RWMutex |  | ||||||
|  |  | ||||||
| 	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,181 +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 != "" { | ||||||
| 		msgID = msg.ID | 		msgid = msg.ID | ||||||
| 		msgReplaceID = 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 { |  | ||||||
| 	if !strings.Contains(b.GetString("Jid"), "@") { |  | ||||||
| 		return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid")) |  | ||||||
| 	} |  | ||||||
| 	tc := &tls.Config{ |  | ||||||
| 		ServerName:         strings.Split(b.GetString("Jid"), "@")[1], |  | ||||||
| 		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 { | ||||||
| @@ -254,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: | ||||||
| @@ -266,74 +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 { | ||||||
| 			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 | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -346,41 +211,30 @@ 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 { | ||||||
| @@ -424,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 |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -135,25 +135,19 @@ func (b *Bzulip) handleQueue() error { | |||||||
| 			if m.SenderEmail == b.GetString("login") { | 			if m.SenderEmail == b.GetString("login") { | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			avatarURL := m.AvatarURL |  | ||||||
| 			if !strings.HasPrefix(avatarURL, "http") { |  | ||||||
| 				avatarURL = b.GetString("server") + avatarURL |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			rmsg := config.Message{ | 			rmsg := config.Message{ | ||||||
| 				Username: m.SenderFullName, | 				Username: m.SenderFullName, | ||||||
| 				Text:     m.Content, | 				Text:     m.Content, | ||||||
| 				Channel:  b.getChannel(m.StreamID) + "/topic:" + m.Subject, | 				Channel:  b.getChannel(m.StreamID) + "/topic:" + m.Subject, | ||||||
| 				Account:  b.Account, | 				Account:  b.Account, | ||||||
| 				UserID:   strconv.Itoa(m.SenderID), | 				UserID:   strconv.Itoa(m.SenderID), | ||||||
| 				Avatar:   avatarURL, | 				Avatar:   m.AvatarURL, | ||||||
| 			} | 			} | ||||||
| 			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) | 			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) | ||||||
| 			b.Log.Debugf("<= Message is %#v", rmsg) | 			b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
| 			b.Remote <- rmsg | 			b.Remote <- rmsg | ||||||
|  | 			b.q.LastEventID = m.ID | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		time.Sleep(time.Second * 3) | 		time.Sleep(time.Second * 3) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										1598
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										1598
									
								
								changelog.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										30
									
								
								ci/bintray.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								ci/bintray.sh
									
									
									
									
									
										Executable 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
									
								
							
							
						
						
									
										17
									
								
								ci/lint.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | #!/usr/bin/env bash | ||||||
|  | set -u -e -x -o pipefail | ||||||
|  |  | ||||||
|  | if [[ -n "${GOLANGCI_VERSION-}" ]]; then | ||||||
|  |   # Retrieve the golangci-lint linter binary. | ||||||
|  |   curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION} | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # Run the linter. | ||||||
|  | golangci-lint run | ||||||
|  |  | ||||||
|  | if [[ "${GO111MODULE-off}" == "on" ]]; then | ||||||
|  |   # If Go modules are active then check that dependencies are correctly maintained. | ||||||
|  |   go mod tidy | ||||||
|  |   go mod vendor | ||||||
|  |   git diff --exit-code --quiet || (echo "Please run 'go mod tidy' to clean up the 'go.mod' and 'go.sum' files."; false) | ||||||
|  | fi | ||||||
							
								
								
									
										17
									
								
								ci/test.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										17
									
								
								ci/test.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | #!/usr/bin/env bash | ||||||
|  | set -u -e -x -o pipefail | ||||||
|  |  | ||||||
|  | if [[ -n "${REPORT_COVERAGE+cover}" ]]; then | ||||||
|  |   # Retrieve and prepare CodeClimate's test coverage reporter. | ||||||
|  |   curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter | ||||||
|  |   chmod +x ./cc-test-reporter | ||||||
|  |   ./cc-test-reporter before-build | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # Run all the tests with the race detector and generate coverage. | ||||||
|  | go test -v -race -coverprofile c.out ./... | ||||||
|  |  | ||||||
|  | if [[ -n "${REPORT_COVERAGE+cover}" && "${TRAVIS_SECURE_ENV_VARS}" == "true" ]]; then | ||||||
|  |   # Upload test coverage to CodeClimate. | ||||||
|  |   ./cc-test-reporter after-build | ||||||
|  | fi | ||||||
| @@ -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") |  | ||||||
| } |  | ||||||
| @@ -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) |  | ||||||
| } |  | ||||||
| @@ -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,"") |  | ||||||
| } |  | ||||||
| @@ -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:] |  | ||||||
| } |  | ||||||
| @@ -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) |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !noapi |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/api" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["api"] = api.New |  | ||||||
| } |  | ||||||
| @@ -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{}{} |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nogitter |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bgitter "github.com/42wim/matterbridge/bridge/gitter" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["gitter"] = bgitter.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !noirc |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	birc "github.com/42wim/matterbridge/bridge/irc" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["irc"] = birc.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nokeybase |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bkeybase "github.com/42wim/matterbridge/bridge/keybase" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["keybase"] = bkeybase.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nomatrix |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bmatrix "github.com/42wim/matterbridge/bridge/matrix" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["matrix"] = bmatrix.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nomattermost |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bmattermost "github.com/42wim/matterbridge/bridge/mattermost" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["mattermost"] = bmattermost.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nomsteams |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bmsteams "github.com/42wim/matterbridge/bridge/msteams" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["msteams"] = bmsteams.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nomumble |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bmumble "github.com/42wim/matterbridge/bridge/mumble" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["mumble"] = bmumble.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nonctalk |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	btalk "github.com/42wim/matterbridge/bridge/nctalk" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["nctalk"] = btalk.New |  | ||||||
| } |  | ||||||
| @@ -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, | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !norocketchat |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	brocketchat "github.com/42wim/matterbridge/bridge/rocketchat" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["rocketchat"] = brocketchat.New |  | ||||||
| } |  | ||||||
| @@ -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{}{} |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nosshchat |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bsshchat "github.com/42wim/matterbridge/bridge/sshchat" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["sshchat"] = bsshchat.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nosteam |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bsteam "github.com/42wim/matterbridge/bridge/steam" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["steam"] = bsteam.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !notelegram |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	btelegram "github.com/42wim/matterbridge/bridge/telegram" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["telegram"] = btelegram.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !novk |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bvk "github.com/42wim/matterbridge/bridge/vk" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["vk"] = bvk.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nowhatsapp |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bwhatsapp "github.com/42wim/matterbridge/bridge/whatsapp" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["whatsapp"] = bwhatsapp.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !noxmpp |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bxmpp "github.com/42wim/matterbridge/bridge/xmpp" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["xmpp"] = bxmpp.New |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| // +build !nozulip |  | ||||||
|  |  | ||||||
| package bridgemap |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	bzulip "github.com/42wim/matterbridge/bridge/zulip" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	FullMap["zulip"] = bzulip.New |  | ||||||
| } |  | ||||||
| @@ -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" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -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 | ||||||
| @@ -127,7 +111,7 @@ func (gw *Gateway) AddConfig(cfg *config.Gateway) error { | |||||||
| 		gw.logger.Errorf("mapChannels() failed: %s", err) | 		gw.logger.Errorf("mapChannels() failed: %s", err) | ||||||
| 	} | 	} | ||||||
| 	for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) { | 	for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) { | ||||||
| 		br := br // scopelint | 		br := br //scopelint | ||||||
| 		err := gw.AddBridge(&br) | 		err := gw.AddBridge(&br) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| @@ -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 | ||||||
| @@ -460,34 +416,17 @@ func (gw *Gateway) SendMessage( | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// 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 | ||||||
| @@ -497,7 +436,7 @@ func (gw *Gateway) SendMessage( | |||||||
| 	if mID != "" { | 	if mID != "" { | ||||||
| 		gw.logger.Debugf("mID %s: %s", dest.Account, mID) | 		gw.logger.Debugf("mID %s: %s", dest.Account, mID) | ||||||
| 		return mID, nil | 		return mID, nil | ||||||
| 		// brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID}) | 		//brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID}) | ||||||
| 	} | 	} | ||||||
| 	return "", nil | 	return "", nil | ||||||
| } | } | ||||||
| @@ -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 |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -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 | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
| 		} | 		} | ||||||
| @@ -131,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 | ||||||
| @@ -144,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) | 				gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs) | ||||||
|  |  | ||||||
| 				// 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) |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
|  | 			idx++ | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										113
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,59 +3,72 @@ module github.com/42wim/matterbridge | |||||||
| require ( | require ( | ||||||
| 	github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 | 	github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 | ||||||
| 	github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f | 	github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f | ||||||
| 	github.com/Jeffail/gabs v1.4.0 // indirect | 	github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 // indirect | ||||||
| 	github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560 | 	github.com/Jeffail/gabs v1.1.1 // indirect | ||||||
| 	github.com/Rhymen/go-whatsapp v0.1.2-0.20210126174449-3c094ebae0ce | 	github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 | ||||||
| 	github.com/SevereCloud/vksdk/v2 v2.9.0 | 	github.com/bwmarrin/discordgo v0.19.0 | ||||||
| 	github.com/d5/tengo/v2 v2.7.0 | 	github.com/d5/tengo v1.20.0 | ||||||
| 	github.com/davecgh/go-spew v1.1.1 | 	github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec | ||||||
| 	github.com/fsnotify/fsnotify v1.4.9 | 	github.com/fsnotify/fsnotify v1.4.7 | ||||||
| 	github.com/go-telegram-bot-api/telegram-bot-api v1.0.1-0.20200524105306-7434b0456e81 | 	github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible | ||||||
| 	github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8 | 	github.com/google/gops v0.3.5 | ||||||
| 	github.com/google/gops v0.3.17 |  | ||||||
| 	github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect | 	github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect | ||||||
| 	github.com/gorilla/schema v1.2.0 | 	github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect | ||||||
| 	github.com/gorilla/websocket v1.4.2 | 	github.com/gorilla/schema v1.0.2 | ||||||
| 	github.com/hashicorp/golang-lru v0.5.4 | 	github.com/gorilla/websocket v1.4.0 | ||||||
| 	github.com/jpillora/backoff v1.0.0 | 	github.com/hashicorp/golang-lru v0.5.0 | ||||||
| 	github.com/keybase/go-keybase-chat-bot v0.0.0-20200505163032-5cacf52379da | 	github.com/hpcloud/tail v1.0.0 // indirect | ||||||
| 	github.com/kyokomi/emoji/v2 v2.2.8 | 	github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 | ||||||
| 	github.com/labstack/echo/v4 v4.2.1 | 	github.com/jtolds/gls v4.2.1+incompatible // indirect | ||||||
| 	github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 | 	github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect | ||||||
| 	github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 | 	github.com/kr/pretty v0.1.0 // indirect | ||||||
| 	github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20210403163225-761e8622445d | 	github.com/labstack/echo/v4 v4.0.0 | ||||||
| 	github.com/matterbridge/discordgo v0.21.2-0.20210201201054-fb39a175b4f7 | 	github.com/lrstanley/girc v0.0.0-20190210212025-51b8e096d398 | ||||||
| 	github.com/matterbridge/go-xmpp v0.0.0-20200418225040-c8a3a57b4050 | 	github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect | ||||||
| 	github.com/matterbridge/gozulipbot v0.0.0-20200820220548-be5824faa913 | 	github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect | ||||||
| 	github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba | 	github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d | ||||||
| 	github.com/mattermost/mattermost-server/v5 v5.30.1 | 	github.com/matterbridge/go-whatsapp v0.0.1-0.20190301204034-f2f1b29d441b | ||||||
| 	github.com/mattn/godown v0.0.1 | 	github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91 | ||||||
| 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect | 	github.com/matterbridge/gomatrix v0.0.0-20190102230110-6f9631ca6dea | ||||||
| 	github.com/missdeer/golib v1.0.4 | 	github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18 | ||||||
| 	github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d // indirect | 	github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61 | ||||||
|  | 	github.com/matterbridge/mautrix-whatsapp v0.0.0-20190301210046-3539cf52ed6e | ||||||
|  | 	github.com/mattermost/mattermost-server v5.5.0+incompatible | ||||||
|  | 	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect | ||||||
|  | 	github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect | ||||||
| 	github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect | 	github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect | ||||||
| 	github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 | 	github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 | ||||||
| 	github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c | 	github.com/nicksnyder/go-i18n v1.4.0 // indirect | ||||||
| 	github.com/rs/xid v1.3.0 | 	github.com/nlopes/slack v0.5.0 | ||||||
| 	github.com/russross/blackfriday v1.6.0 | 	github.com/onsi/ginkgo v1.6.0 // indirect | ||||||
|  | 	github.com/onsi/gomega v1.4.1 // indirect | ||||||
|  | 	github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83 | ||||||
|  | 	github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect | ||||||
|  | 	github.com/peterhellberg/emojilib v0.0.0-20190124112554-c18758d55320 | ||||||
|  | 	github.com/pkg/errors v0.8.0 // indirect | ||||||
|  | 	github.com/rs/xid v1.2.1 | ||||||
|  | 	github.com/russross/blackfriday v1.5.2 | ||||||
| 	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca | 	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca | ||||||
| 	github.com/shazow/ssh-chat v1.10.1 | 	github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296 | ||||||
| 	github.com/sirupsen/logrus v1.8.1 | 	github.com/sirupsen/logrus v1.3.0 | ||||||
| 	github.com/slack-go/slack v0.8.2 | 	github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect | ||||||
| 	github.com/spf13/afero v1.3.4 // indirect | 	github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect | ||||||
| 	github.com/spf13/cast v1.3.1 // indirect | 	github.com/spf13/viper v1.3.1 | ||||||
| 	github.com/spf13/viper v1.7.1 | 	github.com/stretchr/testify v1.3.0 | ||||||
| 	github.com/stretchr/testify v1.7.0 | 	github.com/technoweenie/multipartstreamer v1.0.1 // indirect | ||||||
| 	github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 |  | ||||||
| 	github.com/writeas/go-strip-markdown v2.0.1+incompatible |  | ||||||
| 	github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect | 	github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect | ||||||
| 	github.com/yaegashi/msgraph.go v0.1.4 | 	github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447 | ||||||
| 	github.com/zfjagann/golang-ring v0.0.0-20210116075443-7c86fdb43134 | 	gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a // indirect | ||||||
| 	golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb | 	gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179 // indirect | ||||||
| 	golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 | 	gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f | ||||||
| 	gomod.garykim.dev/nc-talk v0.1.7 | 	gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2 // indirect | ||||||
| 	gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 | 	gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe // indirect | ||||||
| 	layeh.com/gumble v0.0.0-20200818122324-146f9205029b | 	gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 // indirect | ||||||
|  | 	go.uber.org/atomic v1.3.2 // indirect | ||||||
|  | 	go.uber.org/multierr v1.1.0 // indirect | ||||||
|  | 	go.uber.org/zap v1.9.1 // indirect | ||||||
|  | 	golang.org/x/image v0.0.0-20190220214146-31aff87c08e9 | ||||||
|  | 	gopkg.in/fsnotify.v1 v1.4.7 // indirect | ||||||
|  | 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect | ||||||
|  | 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect | ||||||
| ) | ) | ||||||
|  |  | ||||||
| go 1.15 |  | ||||||
|   | |||||||
| @@ -1,288 +0,0 @@ | |||||||
| // Code generated by go-bindata. DO NOT EDIT. |  | ||||||
| // sources: |  | ||||||
| // tengo/outmessage.tengo |  | ||||||
|  |  | ||||||
| package internal |  | ||||||
|  |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"compress/gzip" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func bindataRead(data []byte, name string) ([]byte, error) { |  | ||||||
| 	gz, err := gzip.NewReader(bytes.NewBuffer(data)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("Read %q: %v", name, err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var buf bytes.Buffer |  | ||||||
| 	_, err = io.Copy(&buf, gz) |  | ||||||
| 	clErr := gz.Close() |  | ||||||
|  |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("Read %q: %v", name, err) |  | ||||||
| 	} |  | ||||||
| 	if clErr != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return buf.Bytes(), nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| type asset struct { |  | ||||||
| 	bytes []byte |  | ||||||
| 	info  fileInfoEx |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type fileInfoEx interface { |  | ||||||
| 	os.FileInfo |  | ||||||
| 	MD5Checksum() string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type bindataFileInfo struct { |  | ||||||
| 	name        string |  | ||||||
| 	size        int64 |  | ||||||
| 	mode        os.FileMode |  | ||||||
| 	modTime     time.Time |  | ||||||
| 	md5checksum string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (fi bindataFileInfo) Name() string { |  | ||||||
| 	return fi.name |  | ||||||
| } |  | ||||||
| func (fi bindataFileInfo) Size() int64 { |  | ||||||
| 	return fi.size |  | ||||||
| } |  | ||||||
| func (fi bindataFileInfo) Mode() os.FileMode { |  | ||||||
| 	return fi.mode |  | ||||||
| } |  | ||||||
| func (fi bindataFileInfo) ModTime() time.Time { |  | ||||||
| 	return fi.modTime |  | ||||||
| } |  | ||||||
| func (fi bindataFileInfo) MD5Checksum() string { |  | ||||||
| 	return fi.md5checksum |  | ||||||
| } |  | ||||||
| func (fi bindataFileInfo) IsDir() bool { |  | ||||||
| 	return false |  | ||||||
| } |  | ||||||
| func (fi bindataFileInfo) Sys() interface{} { |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var _bindataTengoOutmessagetengo = []byte( |  | ||||||
| 	"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x91\x3d\x8f\xda\x40\x10\x86\xfb\xfd\x15\x13\x37\xb1\x2d\x07\xe7\xa3" + |  | ||||||
| 	"\xb3\x64\x59\x11\x45\x94\x2e\x8a\x92\x0a\xd0\xb1\xac\x07\x33\xd2\x7a\xc7\x1a\x8f\x31\x88\xe3\xbf\x9f\xcc\x01\x47" + |  | ||||||
| 	"\x7f\xc5\x75\xef\xae\x9e\x9d\x77\x1f\x4d\x9e\x9a\xbd\x15\xb2\x1b\x8f\x3d\xd8\xbd\x25\x3f\x45\x30\x82\xb6\xfe\xc2" + |  | ||||||
| 	"\xc1\x1f\x0b\x43\xe1\xa7\x73\x3c\x04\xcd\x80\xc2\x1f\x61\x65\xc7\x7e\xca\xf3\x9d\x0d\x01\x2f\xf1\x97\x55\x1c\xed" + |  | ||||||
| 	"\xd1\xf0\xa0\x77\x98\x07\x7d\xa3\x79\xd0\x3b\xce\x83\xde\xf8\xd7\x9e\x51\x48\xb1\x30\x6d\xdf\xfc\xc3\x83\x66\xd0" + |  | ||||||
| 	"\xf6\xcd\xff\x1e\x25\xd8\x16\x4d\x9a\x1b\xa3\x78\x50\x28\x4a\xa0\xb6\x63\xd1\x38\x9a\xce\x51\x62\x4c\x9e\x43\xaf" + |  | ||||||
| 	"\x42\x1d\x90\x38\x70\xec\x59\xfa\xe9\x8e\xb6\x30\xe2\x67\x41\x08\xac\xd0\x63\xa8\x29\x34\xa0\x0c\x36\x5c\xc0\x8d" + |  | ||||||
| 	"\x50\xdd\x20\x8c\x78\x7d\xac\x3b\x84\xdf\x7f\xe7\xb7\x01\xb4\x7d\xd0\x84\xb2\x84\x88\xc4\x45\x70\x32\x00\x00\x82" + |  | ||||||
| 	"\xd3\x3f\xa6\xfe\x99\xe0\x93\xe3\xb6\x23\x8f\xf1\x7a\x79\xf8\xfa\x23\xae\x8a\x65\x7d\xfa\x96\x7d\x3f\xc7\x55\x91" + |  | ||||||
| 	"\x5d\x63\x52\x25\xd5\xf3\x62\x51\xb8\xa0\xe2\x8b\xd5\x6a\x9d\x5c\xc6\x5c\x4d\x4b\xc1\x99\x60\xe7\xad\xc3\xf8\x26" + |  | ||||||
| 	"\x1f\x45\x89\x39\x9b\xf7\x6b\xe4\x29\x6d\x1f\x57\x00\x9f\x3e\xc6\x24\xcd\xcd\x4b\x00\x00\x00\xff\xff\x40\xb8\x54" + |  | ||||||
| 	"\xb8\x64\x02\x00\x00") |  | ||||||
|  |  | ||||||
| func bindataTengoOutmessagetengoBytes() ([]byte, error) { |  | ||||||
| 	return bindataRead( |  | ||||||
| 		_bindataTengoOutmessagetengo, |  | ||||||
| 		"tengo/outmessage.tengo", |  | ||||||
| 	) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| func bindataTengoOutmessagetengo() (*asset, error) { |  | ||||||
| 	bytes, err := bindataTengoOutmessagetengoBytes() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	info := bindataFileInfo{ |  | ||||||
| 		name: "tengo/outmessage.tengo", |  | ||||||
| 		size: 612, |  | ||||||
| 		md5checksum: "", |  | ||||||
| 		mode: os.FileMode(420), |  | ||||||
| 		modTime: time.Unix(1555622139, 0), |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	a := &asset{bytes: bytes, info: info} |  | ||||||
|  |  | ||||||
| 	return a, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| // |  | ||||||
| // Asset loads and returns the asset for the given name. |  | ||||||
| // It returns an error if the asset could not be found or |  | ||||||
| // could not be loaded. |  | ||||||
| // |  | ||||||
| func Asset(name string) ([]byte, error) { |  | ||||||
| 	cannonicalName := strings.Replace(name, "\\", "/", -1) |  | ||||||
| 	if f, ok := _bindata[cannonicalName]; ok { |  | ||||||
| 		a, err := f() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) |  | ||||||
| 		} |  | ||||||
| 		return a.bytes, nil |  | ||||||
| 	} |  | ||||||
| 	return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // |  | ||||||
| // MustAsset is like Asset but panics when Asset would return an error. |  | ||||||
| // It simplifies safe initialization of global variables. |  | ||||||
| // nolint: deadcode |  | ||||||
| // |  | ||||||
| func MustAsset(name string) []byte { |  | ||||||
| 	a, err := Asset(name) |  | ||||||
| 	if err != nil { |  | ||||||
| 		panic("asset: Asset(" + name + "): " + err.Error()) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return a |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // |  | ||||||
| // AssetInfo loads and returns the asset info for the given name. |  | ||||||
| // It returns an error if the asset could not be found or could not be loaded. |  | ||||||
| // |  | ||||||
| func AssetInfo(name string) (os.FileInfo, error) { |  | ||||||
| 	cannonicalName := strings.Replace(name, "\\", "/", -1) |  | ||||||
| 	if f, ok := _bindata[cannonicalName]; ok { |  | ||||||
| 		a, err := f() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) |  | ||||||
| 		} |  | ||||||
| 		return a.info, nil |  | ||||||
| 	} |  | ||||||
| 	return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // |  | ||||||
| // AssetNames returns the names of the assets. |  | ||||||
| // nolint: deadcode |  | ||||||
| // |  | ||||||
| func AssetNames() []string { |  | ||||||
| 	names := make([]string, 0, len(_bindata)) |  | ||||||
| 	for name := range _bindata { |  | ||||||
| 		names = append(names, name) |  | ||||||
| 	} |  | ||||||
| 	return names |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // |  | ||||||
| // _bindata is a table, holding each asset generator, mapped to its name. |  | ||||||
| // |  | ||||||
| var _bindata = map[string]func() (*asset, error){ |  | ||||||
| 	"tengo/outmessage.tengo": bindataTengoOutmessagetengo, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // |  | ||||||
| // AssetDir returns the file names below a certain |  | ||||||
| // directory embedded in the file by go-bindata. |  | ||||||
| // For example if you run go-bindata on data/... and data contains the |  | ||||||
| // following hierarchy: |  | ||||||
| //     data/ |  | ||||||
| //       foo.txt |  | ||||||
| //       img/ |  | ||||||
| //         a.png |  | ||||||
| //         b.png |  | ||||||
| // then AssetDir("data") would return []string{"foo.txt", "img"} |  | ||||||
| // AssetDir("data/img") would return []string{"a.png", "b.png"} |  | ||||||
| // AssetDir("foo.txt") and AssetDir("notexist") would return an error |  | ||||||
| // AssetDir("") will return []string{"data"}. |  | ||||||
| // |  | ||||||
| func AssetDir(name string) ([]string, error) { |  | ||||||
| 	node := _bintree |  | ||||||
| 	if len(name) != 0 { |  | ||||||
| 		cannonicalName := strings.Replace(name, "\\", "/", -1) |  | ||||||
| 		pathList := strings.Split(cannonicalName, "/") |  | ||||||
| 		for _, p := range pathList { |  | ||||||
| 			node = node.Children[p] |  | ||||||
| 			if node == nil { |  | ||||||
| 				return nil, &os.PathError{ |  | ||||||
| 					Op: "open", |  | ||||||
| 					Path: name, |  | ||||||
| 					Err: os.ErrNotExist, |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	if node.Func != nil { |  | ||||||
| 		return nil, &os.PathError{ |  | ||||||
| 			Op: "open", |  | ||||||
| 			Path: name, |  | ||||||
| 			Err: os.ErrNotExist, |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	rv := make([]string, 0, len(node.Children)) |  | ||||||
| 	for childName := range node.Children { |  | ||||||
| 		rv = append(rv, childName) |  | ||||||
| 	} |  | ||||||
| 	return rv, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| type bintree struct { |  | ||||||
| 	Func     func() (*asset, error) |  | ||||||
| 	Children map[string]*bintree |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var _bintree = &bintree{Func: nil, Children: map[string]*bintree{ |  | ||||||
| 	"tengo": {Func: nil, Children: map[string]*bintree{ |  | ||||||
| 		"outmessage.tengo": {Func: bindataTengoOutmessagetengo, Children: map[string]*bintree{}}, |  | ||||||
| 	}}, |  | ||||||
| }} |  | ||||||
|  |  | ||||||
| // RestoreAsset restores an asset under the given directory |  | ||||||
| func RestoreAsset(dir, name string) error { |  | ||||||
| 	data, err := Asset(name) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	info, err := AssetInfo(name) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // RestoreAssets restores an asset under the given directory recursively |  | ||||||
| func RestoreAssets(dir, name string) error { |  | ||||||
| 	children, err := AssetDir(name) |  | ||||||
| 	// File |  | ||||||
| 	if err != nil { |  | ||||||
| 		return RestoreAsset(dir, name) |  | ||||||
| 	} |  | ||||||
| 	// Dir |  | ||||||
| 	for _, child := range children { |  | ||||||
| 		err = RestoreAssets(dir, filepath.Join(name, child)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func _filePath(dir, name string) string { |  | ||||||
| 	cannonicalName := strings.Replace(name, "\\", "/", -1) |  | ||||||
| 	return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) |  | ||||||
| } |  | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| /* |  | ||||||
| variables available  |  | ||||||
| read-only: |  | ||||||
| inAccount, inProtocol, inChannel, inGateway |  | ||||||
| outAccount, outProtocol, outChannel, outGateway |  | ||||||
|  |  | ||||||
| read-write: |  | ||||||
| msgText, msgUsername |  | ||||||
| */ |  | ||||||
|  |  | ||||||
| text := import("text") |  | ||||||
|  |  | ||||||
| // start - strip irc colors  |  | ||||||
| // if we're not sending to an irc bridge we strip the IRC colors |  | ||||||
| if inProtocol == "irc" && outProtocol != "irc" { |  | ||||||
|     re := text.re_compile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`) |  | ||||||
|     msgText=re.replace(msgText,"") |  | ||||||
| } |  | ||||||
| // end - strip irc colors |  | ||||||
|  |  | ||||||
| // strip custom emoji |  | ||||||
| if inProtocol == "discord" { |  | ||||||
|     re := text.re_compile(`<a?(:.*?:)[0-9]+>`) |  | ||||||
|     msgText=re.replace(msgText,"$1") |  | ||||||
| } |  | ||||||
| @@ -4,7 +4,6 @@ import ( | |||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"runtime" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| @@ -16,7 +15,7 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	version = "1.22.1" | 	version = "1.14.4" | ||||||
| 	githash string | 	githash string | ||||||
|  |  | ||||||
| 	flagConfig  = flag.String("conf", "matterbridge.toml", "config file") | 	flagConfig  = flag.String("conf", "matterbridge.toml", "config file") | ||||||
| @@ -51,15 +50,6 @@ func main() { | |||||||
| 	cfg := config.NewConfig(rootLogger, *flagConfig) | 	cfg := config.NewConfig(rootLogger, *flagConfig) | ||||||
| 	cfg.BridgeValues().General.Debug = *flagDebug | 	cfg.BridgeValues().General.Debug = *flagDebug | ||||||
|  |  | ||||||
| 	// if logging to a file, ensure it is closed when the program terminates |  | ||||||
| 	// nolint:errcheck |  | ||||||
| 	defer func() { |  | ||||||
| 		if f, ok := rootLogger.Out.(*os.File); ok { |  | ||||||
| 			f.Sync() |  | ||||||
| 			f.Close() |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	r, err := gateway.NewRouter(rootLogger, cfg, bridgemap.FullMap) | 	r, err := gateway.NewRouter(rootLogger, cfg, bridgemap.FullMap) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logger.Fatalf("Starting gateway failed: %s", err) | 		logger.Fatalf("Starting gateway failed: %s", err) | ||||||
| @@ -77,31 +67,17 @@ func setupLogger() *logrus.Logger { | |||||||
| 		Formatter: &prefixed.TextFormatter{ | 		Formatter: &prefixed.TextFormatter{ | ||||||
| 			PrefixPadding: 13, | 			PrefixPadding: 13, | ||||||
| 			DisableColors: true, | 			DisableColors: true, | ||||||
|  | 			FullTimestamp: true, | ||||||
| 		}, | 		}, | ||||||
| 		Level: logrus.InfoLevel, | 		Level: logrus.InfoLevel, | ||||||
| 	} | 	} | ||||||
| 	if *flagDebug || os.Getenv("DEBUG") == "1" { | 	if *flagDebug || os.Getenv("DEBUG") == "1" { | ||||||
| 		logger.SetReportCaller(true) |  | ||||||
| 		logger.Formatter = &prefixed.TextFormatter{ | 		logger.Formatter = &prefixed.TextFormatter{ | ||||||
| 			PrefixPadding: 13, | 			PrefixPadding:   13, | ||||||
| 			DisableColors: true, | 			DisableColors:   true, | ||||||
| 			FullTimestamp: false, | 			FullTimestamp:   false, | ||||||
|  | 			ForceFormatting: true, | ||||||
| 			CallerFormatter: func(function, file string) string { |  | ||||||
| 				return fmt.Sprintf(" [%s:%s]", function, file) |  | ||||||
| 			}, |  | ||||||
| 			CallerPrettyfier: func(f *runtime.Frame) (string, string) { |  | ||||||
| 				sp := strings.SplitAfter(f.File, "/matterbridge/") |  | ||||||
| 				filename := f.File |  | ||||||
| 				if len(sp) > 1 { |  | ||||||
| 					filename = sp[1] |  | ||||||
| 				} |  | ||||||
| 				s := strings.Split(f.Function, ".") |  | ||||||
| 				funcName := s[len(s)-1] |  | ||||||
| 				return funcName, fmt.Sprintf("%s:%d", filename, f.Line) |  | ||||||
| 			}, |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		logger.Level = logrus.DebugLevel | 		logger.Level = logrus.DebugLevel | ||||||
| 		logger.WithFields(logrus.Fields{"prefix": "main"}).Info("Enabling debug logging.") | 		logger.WithFields(logrus.Fields{"prefix": "main"}).Info("Enabling debug logging.") | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| #This is configuration for matterbridge. | #This is configuration for matterbridge. | ||||||
| #WARNING: as this file contains credentials, be sure to set correct file permissions | #WARNING: as this file contains credentials, be sure to set correct file permissions | ||||||
| #See https://github.com/42wim/matterbridge/wiki/How-to-create-your-config for how to create your config |  | ||||||
| #See https://github.com/42wim/matterbridge/wiki/Settings for all settings |  | ||||||
| ################################################################### | ################################################################### | ||||||
| #IRC section | #IRC section | ||||||
| ################################################################### | ################################################################### | ||||||
| @@ -103,16 +101,6 @@ ColorNicks=false | |||||||
| #OPTIONAL (default empty) | #OPTIONAL (default empty) | ||||||
| RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"] | RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"] | ||||||
|  |  | ||||||
| #PingDelay specifies how long to wait to send a ping to the irc server. |  | ||||||
| #You can use s for second, m for minute |  | ||||||
| #String |  | ||||||
| #OPTIONAL (default 1m) |  | ||||||
| PingDelay="1m" |  | ||||||
|  |  | ||||||
| #StripMarkdown strips markdown from messages |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| StripMarkdown=false |  | ||||||
|  |  | ||||||
| #Nicks you want to ignore.  | #Nicks you want to ignore.  | ||||||
| #Regular expressions supported | #Regular expressions supported | ||||||
| #Messages from those users will not be sent to other bridges. | #Messages from those users will not be sent to other bridges. | ||||||
| @@ -167,11 +155,6 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | |||||||
| #OPTIONAL (default false) | #OPTIONAL (default false) | ||||||
| ShowJoinPart=false | ShowJoinPart=false | ||||||
|  |  | ||||||
| #Enable to show verbose users joins/parts (ident@host) from other bridges |  | ||||||
| #Currently works for messages from the following bridges: irc |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| VerboseJoinPart=false |  | ||||||
|  |  | ||||||
| #Do not send joins/parts to other bridges | #Do not send joins/parts to other bridges | ||||||
| #Currently works for messages from the following bridges: irc, mattermost, slack | #Currently works for messages from the following bridges: irc, mattermost, slack | ||||||
| #OPTIONAL (default false) | #OPTIONAL (default false) | ||||||
| @@ -187,25 +170,6 @@ StripNick=false | |||||||
| #OPTIONAL (default false) | #OPTIONAL (default false) | ||||||
| ShowTopicChange=false | ShowTopicChange=false | ||||||
|  |  | ||||||
| #Delay in milliseconds between channel joins |  | ||||||
| #Only useful when you have a LOT of channels to join |  | ||||||
| #See https://github.com/42wim/matterbridge/issues/1084 |  | ||||||
| #OPTIONAL (default 0) |  | ||||||
| JoinDelay=0 |  | ||||||
|  |  | ||||||
| #Use the optional RELAYMSG extension for username spoofing on IRC. |  | ||||||
| #This requires an IRCd that supports the draft/relaymsg specification: currently this includes |  | ||||||
| #Oragono 2.4.0+ and InspIRCd 3 with the m_relaymsg contrib module. |  | ||||||
| #See https://github.com/42wim/matterbridge/issues/667#issuecomment-634214165 for more details. |  | ||||||
| #Spoofed nicks will use the configured RemoteNickFormat, replacing reserved IRC characters |  | ||||||
| #(!+%@&#$:'"?*,.) with a hyphen (-). |  | ||||||
| #On most configurations, the RemoteNickFormat must include a separator character such as "/". |  | ||||||
| #You should make sure that the settings here match your IRCd. |  | ||||||
| #This option overrides ColorNicks. |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| UseRelayMsg=false |  | ||||||
| #RemoteNickFormat="{NICK}/{PROTOCOL}" |  | ||||||
|  |  | ||||||
| ################################################################### | ################################################################### | ||||||
| #XMPP section | #XMPP section | ||||||
| ################################################################### | ################################################################### | ||||||
| @@ -240,10 +204,6 @@ Nick="xmppbot" | |||||||
| #OPTIONAL (default false) | #OPTIONAL (default false) | ||||||
| SkipTLSVerify=true | SkipTLSVerify=true | ||||||
|  |  | ||||||
| #Enable to use plaintext connection to your XMPP server. |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| NoTLS=true |  | ||||||
|  |  | ||||||
| ## RELOADABLE SETTINGS | ## RELOADABLE SETTINGS | ||||||
| ## Settings below can be reloaded by editing the file | ## Settings below can be reloaded by editing the file | ||||||
|  |  | ||||||
| @@ -310,10 +270,97 @@ StripNick=false | |||||||
| #OPTIONAL (default false) | #OPTIONAL (default false) | ||||||
| ShowTopicChange=false | ShowTopicChange=false | ||||||
|  |  | ||||||
| #Enable sending messages using a webhook instead of regular MUC messages. | ################################################################### | ||||||
| #Only works with a prosody server using mod_slack_webhook. Does not support editing. | #hipchat section | ||||||
| #OPTIONAL (default "") | ################################################################### | ||||||
| WebhookURL="https://yourdomain/prosody/msg/someid" | #Go to https://www.hipchat.com/account/xmpp this will show you the necessary data | ||||||
|  | #to fill in the section below | ||||||
|  | [xmpp.hipchat] | ||||||
|  | #xmpp server to connect to.  | ||||||
|  | #REQUIRED | ||||||
|  | Server="chat.hipchat.com:5222" | ||||||
|  |  | ||||||
|  | #Jabber ID | ||||||
|  | #REQUIRED | ||||||
|  | Jid="12345_12345@chat.hipchat.com" | ||||||
|  |  | ||||||
|  | #Password (your hipchat password) | ||||||
|  | #REQUIRED | ||||||
|  | Password="yourpass" | ||||||
|  |  | ||||||
|  | #Conference (MUC) domain | ||||||
|  | #REQUIRED | ||||||
|  | Muc="conf.hipchat.com" | ||||||
|  |  | ||||||
|  | #Room nickname | ||||||
|  | #REQUIRED | ||||||
|  | Nick="yourlogin" | ||||||
|  |  | ||||||
|  | ## RELOADABLE SETTINGS | ||||||
|  | ## Settings below can be reloaded by editing the file | ||||||
|  |  | ||||||
|  | #Nicks you want to ignore.  | ||||||
|  | #Regular expressions supported | ||||||
|  | #Messages from those users will not be sent to other bridges. | ||||||
|  | #OPTIONAL | ||||||
|  | IgnoreNicks="spammer1 spammer2" | ||||||
|  |  | ||||||
|  | #Messages you want to ignore.  | ||||||
|  | #Messages matching these regexp will be ignored and not sent to other bridges | ||||||
|  | #See https://regex-golang.appspot.com/assets/html/index.html for more regex info | ||||||
|  | #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword | ||||||
|  | IgnoreMessages="^~~ badword" | ||||||
|  |  | ||||||
|  | #messages you want to replace. | ||||||
|  | #it replaces outgoing messages from the bridge. | ||||||
|  | #so you need to place it by the sending bridge definition. | ||||||
|  | #regular expressions supported | ||||||
|  | #some examples: | ||||||
|  | #this replaces cat => dog and sleep => awake | ||||||
|  | #replacemessages=[ ["cat","dog"], ["sleep","awake"] ] | ||||||
|  | #this replaces every number with number.  123 => numbernumbernumber | ||||||
|  | #replacemessages=[ ["[0-9]","number"] ] | ||||||
|  | #optional (default empty) | ||||||
|  | ReplaceMessages=[ ["cat","dog"] ] | ||||||
|  |  | ||||||
|  | #nicks you want to replace. | ||||||
|  | #see replacemessages for syntaxa | ||||||
|  | #optional (default empty) | ||||||
|  | ReplaceNicks=[ ["user--","user"] ] | ||||||
|  |  | ||||||
|  | #Extractnicks is used to for example rewrite messages from other relaybots | ||||||
|  | #See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466 | ||||||
|  | #some examples: | ||||||
|  | #this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting" | ||||||
|  | #ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ] | ||||||
|  | #you can use multiple entries for multiplebots | ||||||
|  | #this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else" | ||||||
|  | #ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ] | ||||||
|  | #OPTIONAL (default empty) | ||||||
|  | ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ] | ||||||
|  |  | ||||||
|  | #extra label that can be used in the RemoteNickFormat | ||||||
|  | #optional (default empty) | ||||||
|  | Label="" | ||||||
|  |  | ||||||
|  | #RemoteNickFormat defines how remote users appear on this bridge  | ||||||
|  | #See [general] config section for default options | ||||||
|  | RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> " | ||||||
|  |  | ||||||
|  | #Enable to show users joins/parts from other bridges  | ||||||
|  | #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||||
|  | #OPTIONAL (default false) | ||||||
|  | ShowJoinPart=false | ||||||
|  |  | ||||||
|  | #StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285 | ||||||
|  | #It will strip other characters from the nick | ||||||
|  | #OPTIONAL (default false) | ||||||
|  | StripNick=false | ||||||
|  |  | ||||||
|  | #Enable to show topic changes from other bridges  | ||||||
|  | #Only works hiding/show topic changes from slack bridge for now | ||||||
|  | #OPTIONAL (default false) | ||||||
|  | ShowTopicChange=false | ||||||
|  |  | ||||||
| ################################################################### | ################################################################### | ||||||
| #mattermost section | #mattermost section | ||||||
| @@ -388,12 +435,6 @@ NickFormatter="plain" | |||||||
| #OPTIONAL (default 4) | #OPTIONAL (default 4) | ||||||
| NicksPerRow=4 | NicksPerRow=4 | ||||||
|  |  | ||||||
| #Skip the Mattermost server version checks that are normally done when connecting. |  | ||||||
| #The usage scenario for this feature would be when the Mattermost instance is hosted behind a |  | ||||||
| #reverse proxy that suppresses "non-standard" response headers in flight. |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| SkipVersionCheck=false |  | ||||||
|  |  | ||||||
| #Whether to prefix messages from other bridges to mattermost with the sender's nick.  | #Whether to prefix messages from other bridges to mattermost with the sender's nick.  | ||||||
| #Useful if username overrides for incoming webhooks isn't enabled on the  | #Useful if username overrides for incoming webhooks isn't enabled on the  | ||||||
| #mattermost server. If you set PrefixMessagesWithNick to true, each message  | #mattermost server. If you set PrefixMessagesWithNick to true, each message  | ||||||
| @@ -560,119 +601,6 @@ StripNick=false | |||||||
| #OPTIONAL (default false) | #OPTIONAL (default false) | ||||||
| ShowTopicChange=false | ShowTopicChange=false | ||||||
|  |  | ||||||
| ################################################################### |  | ||||||
| # |  | ||||||
| # Keybase |  | ||||||
| # You should have a separate bridge account on Keybase |  | ||||||
| # (it also needs to be logged in on the system you're running the bridge on) |  | ||||||
| # |  | ||||||
| ################################################################### |  | ||||||
|  |  | ||||||
| [keybase.myteam] |  | ||||||
|  |  | ||||||
| # RemoteNickFormat defines how remote users appear on this bridge |  | ||||||
| # See [general] config section for default options |  | ||||||
| RemoteNickFormat="{NICK} ({PROTOCOL}): " |  | ||||||
|  |  | ||||||
| # extra label that can be used in the RemoteNickFormat |  | ||||||
| # optional (default empty) |  | ||||||
| Label="" |  | ||||||
|  |  | ||||||
| # Your team on Keybase. |  | ||||||
| # The bot user MUST be a member of this team |  | ||||||
| # REQUIRED |  | ||||||
| Team="myteam" |  | ||||||
|  |  | ||||||
| ################################################################### |  | ||||||
| # Microsoft teams section |  | ||||||
| # See https://github.com/42wim/matterbridge/wiki/MS-Teams-setup |  | ||||||
| ################################################################### |  | ||||||
|  |  | ||||||
| [msteams.myteam] |  | ||||||
|  |  | ||||||
| # TenantID |  | ||||||
| # See https://github.com/42wim/matterbridge/wiki/MS-Teams-setup#get-necessary-ids-for-matterbridge |  | ||||||
| TenantID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" |  | ||||||
|  |  | ||||||
| # ClientID |  | ||||||
| # See https://github.com/42wim/matterbridge/wiki/MS-Teams-setup#get-necessary-ids-for-matterbridge |  | ||||||
| ClientID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" |  | ||||||
|  |  | ||||||
| # TeamID |  | ||||||
| # See https://github.com/42wim/matterbridge/wiki/MS-Teams-setup#get-necessary-ids-for-matterbridge |  | ||||||
| TeamID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" |  | ||||||
|  |  | ||||||
| ## RELOADABLE SETTINGS |  | ||||||
| ## Settings below can be reloaded by editing the file |  | ||||||
|  |  | ||||||
| #Nicks you want to ignore. |  | ||||||
| #Regular expressions supported |  | ||||||
| #Messages from those users will not be sent to other bridges. |  | ||||||
| #OPTIONAL |  | ||||||
| IgnoreNicks="ircspammer1 ircspammer2" |  | ||||||
|  |  | ||||||
| #Messages you want to ignore. |  | ||||||
| #Messages matching these regexp will be ignored and not sent to other bridges |  | ||||||
| #See https://regex-golang.appspot.com/assets/html/index.html for more regex info |  | ||||||
| #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword |  | ||||||
| IgnoreMessages="^~~ badword" |  | ||||||
|  |  | ||||||
| #messages you want to replace. |  | ||||||
| #it replaces outgoing messages from the bridge. |  | ||||||
| #so you need to place it by the sending bridge definition. |  | ||||||
| #regular expressions supported |  | ||||||
| #some examples: |  | ||||||
| #this replaces cat => dog and sleep => awake |  | ||||||
| #replacemessages=[ ["cat","dog"], ["sleep","awake"] ] |  | ||||||
| #this replaces every number with number.  123 => numbernumbernumber |  | ||||||
| #replacemessages=[ ["[0-9]","number"] ] |  | ||||||
| #optional (default empty) |  | ||||||
| ReplaceMessages=[ ["cat","dog"] ] |  | ||||||
|  |  | ||||||
| #nicks you want to replace. |  | ||||||
| #see replacemessages for syntaxa |  | ||||||
| #optional (default empty) |  | ||||||
| ReplaceNicks=[ ["user--","user"] ] |  | ||||||
|  |  | ||||||
| #Extractnicks is used to for example rewrite messages from other relaybots |  | ||||||
| #See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466 |  | ||||||
| #some examples: |  | ||||||
| #this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting" |  | ||||||
| #ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ] |  | ||||||
| #you can use multiple entries for multiplebots |  | ||||||
| #this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else" |  | ||||||
| #ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ] |  | ||||||
| #OPTIONAL (default empty) |  | ||||||
| ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ] |  | ||||||
|  |  | ||||||
| #extra label that can be used in the RemoteNickFormat |  | ||||||
| #optional (default empty) |  | ||||||
| Label="" |  | ||||||
|  |  | ||||||
| #RemoteNickFormat defines how remote users appear on this bridge |  | ||||||
| #See [general] config section for default options |  | ||||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " |  | ||||||
|  |  | ||||||
| #Enable to show users joins/parts from other bridges |  | ||||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| ShowJoinPart=false |  | ||||||
|  |  | ||||||
| #StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285 |  | ||||||
| #It will strip other characters from the nick |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| StripNick=false |  | ||||||
|  |  | ||||||
| #Enable to show topic changes from other bridges |  | ||||||
| #Only works hiding/show topic changes from slack bridge for now |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| ShowTopicChange=false |  | ||||||
|  |  | ||||||
| #Opportunistically preserve threaded replies between bridges |  | ||||||
| #that support threading |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| PreserveThreading=false |  | ||||||
|  |  | ||||||
| ################################################################### | ################################################################### | ||||||
| #slack section | #slack section | ||||||
| ################################################################### | ################################################################### | ||||||
| @@ -831,126 +759,114 @@ ShowUserTyping=false | |||||||
| ################################################################### | ################################################################### | ||||||
| [discord] | [discord] | ||||||
|  |  | ||||||
| # You can configure multiple servers "[discord.name]" or "[discord.name2]" | #You can configure multiple servers "[discord.name]" or "[discord.name2]" | ||||||
| # In this example we use [discord.game] | #In this example we use [discord.game] | ||||||
| #REQUIRED | #REQUIRED | ||||||
| [discord.game] | [discord.game] | ||||||
| # Token (REQUIRED) is the token to connect with Discord API | #Token to connect with Discord API | ||||||
| # You can get your token by following the instructions on | #You can get your token by following the instructions on  | ||||||
| # https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token | #https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token | ||||||
| # If you want roles/groups mentions to be shown with names instead of ID, you'll need to give your bot the "Manage Roles" permission. | #If you want roles/groups mentions to be shown with names instead of ID, you'll need to give your bot the "Manage Roles" permission. | ||||||
|  | #REQUIRED | ||||||
| Token="Yourtokenhere" | Token="Yourtokenhere" | ||||||
|  |  | ||||||
| # Server (REQUIRED) is the ID or name of the guild to connect to, selected from the guilds the bot has been invited to | #REQUIRED | ||||||
| Server="yourservername" | Server="yourservername" | ||||||
|  |  | ||||||
| ## RELOADABLE SETTINGS | ## RELOADABLE SETTINGS | ||||||
| ## All settings below can be reloaded by editing the file. | ## Settings below can be reloaded by editing the file | ||||||
| ## They are also all optional. |  | ||||||
|  |  | ||||||
| # ShowEmbeds shows the title, description and URL of embedded messages (sent by other bots) | #Shows title, description and URL of embedded messages (sent by other bots) | ||||||
|  | #OPTIONAL (default false) | ||||||
| ShowEmbeds=false | ShowEmbeds=false | ||||||
|  |  | ||||||
| # UseLocalAvatar specifies source bridges for which an avatar should be 'guessed' when an incoming message has no avatar. | #Shows the username instead of the server nickname | ||||||
| # This works by comparing the username of the message to an existing Discord user, and using the avatar of the Discord user. | #OPTIONAL (default false) | ||||||
| # |  | ||||||
| # This only works if WebhookURL is set (AND the message has no avatar). |  | ||||||
| # Example: ["irc"] |  | ||||||
| UseLocalAvatar=[] |  | ||||||
|  |  | ||||||
| # UseUserName shows the username instead of the server nickname |  | ||||||
| UseUserName=false | UseUserName=false | ||||||
|  |  | ||||||
| # UseDiscriminator appends the `#xxxx` discriminator when used with UseUserName | #Show #xxxx discriminator with UseUserName | ||||||
|  | #OPTIONAL (default false) | ||||||
| UseDiscriminator=false | UseDiscriminator=false | ||||||
|  |  | ||||||
| # AutoWebhooks automatically configures message sending in the style of puppets. | #Specify WebhookURL. If given, will relay messages using the Webhook, which gives a better look to messages. | ||||||
| # This is an easier alternative to manually configuring "WebhookURL" for each gateway, | #This only works if you have one discord channel, if you have multiple discord channels you'll have to specify it in the gateway config | ||||||
| # as turning this on will automatically load or create webhooks for each channel. | #OPTIONAL (default empty) | ||||||
| # This feature requires the "Manage Webhooks" permission (either globally or as per-channel). | WebhookURL="Yourwebhooktokenhere" | ||||||
| AutoWebhooks=false |  | ||||||
|  |  | ||||||
| # EditDisable disables sending of edits to other bridges | #Disable sending of edits to other bridges | ||||||
|  | #OPTIONAL (default false) | ||||||
| EditDisable=false | EditDisable=false | ||||||
|  |  | ||||||
| # EditSuffix specifies the message to be appended to every edited message | #Message to be appended to every edited message | ||||||
| # Example: " (edited)" | #OPTIONAL (default empty) | ||||||
| EditSuffix="" | EditSuffix=" (edited)" | ||||||
|  |  | ||||||
| # IgnoreNicks mutes outgoing messages from certain users. | #Nicks you want to ignore.  | ||||||
| # Messages from these users will not be transmitted to other bridges. | #Regular expressions supported | ||||||
| # Regular expressions are also supported. | #Messages from those users will not be sent to other bridges. | ||||||
| # Example: "ircspammer1 ircspammer2" | #OPTIONAL | ||||||
| IgnoreNicks="" | IgnoreNicks="ircspammer1 ircspammer2" | ||||||
|  |  | ||||||
| # IgnoreMessages mutes outgoing messages of a certain format. | #Messages you want to ignore.  | ||||||
| # Messages matching this regular expression will not be transmitted sent to other bridges | #Messages matching these regexp will be ignored and not sent to other bridges | ||||||
| # See https://regex-golang.appspot.com/assets/html/index.html for more regex info | #See https://regex-golang.appspot.com/assets/html/index.html for more regex info | ||||||
| # | #OPTIONAL (example below ignores messages starting with ~~ or messages containing badword | ||||||
| # Example that ignores messages starting with ~~ or messages containing badword: | IgnoreMessages="^~~ badword" | ||||||
| #   IgnoreMessages="^~~ badword" |  | ||||||
| IgnoreMessages="" |  | ||||||
|  |  | ||||||
| # ReplaceMessages replaces substrings of messages in outgoing messages. | #messages you want to replace. | ||||||
| # Regular expressions are supported. | #it replaces outgoing messages from the bridge. | ||||||
| # | #so you need to place it by the sending bridge definition. | ||||||
| # Example that replaces 'cat' => 'dog' and 'sleep' => 'awake': | #regular expressions supported | ||||||
| #   ReplaceMessages=[ ["cat","dog"], ["sleep","awake"] ] | #some examples: | ||||||
| # Example that replaces all digits with the letter 'X', so 'hello123' becomes 'helloXXX': | #this replaces cat => dog and sleep => awake | ||||||
| #   ReplaceMessages=[ ["[0-9]","X"] ] | #replacemessages=[ ["cat","dog"], ["sleep","awake"] ] | ||||||
| ReplaceMessages=[] | #this replaces every number with number.  123 => numbernumbernumber | ||||||
|  | #replacemessages=[ ["[0-9]","number"] ] | ||||||
|  | #optional (default empty) | ||||||
|  | ReplaceMessages=[ ["cat","dog"] ] | ||||||
|  |  | ||||||
| # ReplaceNicks replaces substrings of usernames in outgoing messages. | #nicks you want to replace. | ||||||
| # See the ReplaceMessages setting for examples. | #see replacemessages for syntaxa | ||||||
| # Example: [ ["user--","user"] ] | #optional (default empty) | ||||||
| ReplaceNicks=[] | ReplaceNicks=[ ["user--","user"] ] | ||||||
|  |  | ||||||
| # ExtractNicks allows for interoperability with other bridge software by rewriting messages and extracting usernames. | #Extractnicks is used to for example rewrite messages from other relaybots | ||||||
| # | #See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466 | ||||||
| # Recommended reading: | #some examples: | ||||||
| # - https://github.com/42wim/matterbridge/issues/466 | #this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting" | ||||||
| # - https://github.com/42wim/matterbridge/issues/713 | #ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ] | ||||||
| # | #you can use multiple entries for multiplebots | ||||||
| # This example translates the following message | #this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else" | ||||||
| #   "Relaybot: <relayeduser> something interesting" | #ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ] | ||||||
| # into this message | #OPTIONAL (default empty) | ||||||
| #   "relayeduser: something interesting" | ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ] | ||||||
| # like so: |  | ||||||
| #   ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ] |  | ||||||
| # |  | ||||||
| # This example translates the following message |  | ||||||
| #   "otherbot: (relayeduser) something else" |  | ||||||
| # into this message |  | ||||||
| #   "relayeduser: something else" |  | ||||||
| # like so: |  | ||||||
| #   ExtractNicks=[ [ "otherbot","\\((.*?)\\)\\s+" ] ] |  | ||||||
| # |  | ||||||
| # This example combines both of the above examples into one: |  | ||||||
| #   ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ] |  | ||||||
| # |  | ||||||
| ExtractNicks=[] |  | ||||||
|  |  | ||||||
| # Label is as an extra identifier for use in the RemoteNickFormat setting. | #extra label that can be used in the RemoteNickFormat | ||||||
|  | #optional (default empty) | ||||||
| Label="" | Label="" | ||||||
|  |  | ||||||
| # RemoteNickFormat formats how remote users appear on this bridge. | #RemoteNickFormat defines how remote users appear on this bridge  | ||||||
| # See the [general] config section for default options | #See [general] config section for default options | ||||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||||
|  |  | ||||||
| # ShowJoinPart emits messages that show joins/parts from other bridges | #Enable to show users joins/parts from other bridges  | ||||||
| # Supported from the following bridges: irc, mattermost, slack, discord | #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||||
|  | #OPTIONAL (default false) | ||||||
| ShowJoinPart=false | ShowJoinPart=false | ||||||
|  |  | ||||||
| # StripNick strips non-alphanumeric characters from nicknames. | #StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285 | ||||||
| # Recommended reading: https://github.com/42wim/matterbridge/issues/285 | #It will strip other characters from the nick | ||||||
|  | #OPTIONAL (default false) | ||||||
| StripNick=false | StripNick=false | ||||||
|  |  | ||||||
| # ShowTopicChange emits messages that show topic/purpose updates from other bridges | #Enable to show topic/purpose changes from other bridges | ||||||
| # Supported from the following bridges: slack | #Only works hiding/show topic changes from slack bridge for now | ||||||
|  | #OPTIONAL (default false) | ||||||
| ShowTopicChange=false | ShowTopicChange=false | ||||||
|  |  | ||||||
| # SyncTopic synchronises topic/purpose updates from other bridges | #Enable to sync topic/purpose changes from other bridges | ||||||
| # Supported from the following bridges: slack | #Only works syncing topic changes from slack bridge for now | ||||||
|  | #OPTIONAL (default false) | ||||||
| SyncTopic=false | SyncTopic=false | ||||||
|  |  | ||||||
| ################################################################### | ################################################################### | ||||||
| @@ -971,17 +887,12 @@ Token="Yourtokenhere" | |||||||
| ## Settings below can be reloaded by editing the file | ## Settings below can be reloaded by editing the file | ||||||
|  |  | ||||||
| #OPTIONAL (default empty) | #OPTIONAL (default empty) | ||||||
| #Supported formats are: | #Supported formats are "HTML", "Markdown" and "HTMLNick" | ||||||
| #"HTML" https://core.telegram.org/bots/api#html-style | #See https://core.telegram.org/bots/api#html-style | ||||||
| #"Markdown" https://core.telegram.org/bots/api#markdown-style - deprecated, doesn't display links with underscores correctly | #See https://core.telegram.org/bots/api#markdown-style | ||||||
| #"MarkdownV2" https://core.telegram.org/bots/api#markdownv2-style | #HTMLNick only allows HTML for the nick, the message itself will be html-escaped | ||||||
| #"HTMLNick" - only allows HTML for the nick, the message itself will be html-escaped |  | ||||||
| MessageFormat="" | MessageFormat="" | ||||||
|  |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| #Disables link previews for links in messages |  | ||||||
| DisableWebPagePreview=false |  | ||||||
|  |  | ||||||
| #If enabled use the "First Name" as username. If this is empty use the Username | #If enabled use the "First Name" as username. If this is empty use the Username | ||||||
| #If disabled use the "Username" as username. If this is empty use the First Name  | #If disabled use the "Username" as username. If this is empty use the First Name  | ||||||
| #If all names are empty, username will be "unknown" | #If all names are empty, username will be "unknown" | ||||||
| @@ -998,10 +909,6 @@ UseInsecureURL=false | |||||||
| #OPTIONAL (default false) | #OPTIONAL (default false) | ||||||
| QuoteDisable=false | QuoteDisable=false | ||||||
|  |  | ||||||
| #Set the max. quoted length if 0 the whole message will be quoted |  | ||||||
| #OPTIONAL (default 0) |  | ||||||
| QuoteLengthLimit=0 |  | ||||||
|  |  | ||||||
| #Format quoted/reply messages | #Format quoted/reply messages | ||||||
| #OPTIONAL (default "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})") | #OPTIONAL (default "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})") | ||||||
| QuoteFormat="{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" | QuoteFormat="{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" | ||||||
| @@ -1105,11 +1012,6 @@ Server="https://yourrocketchatserver.domain.com:443" | |||||||
| #REQUIRED (when not using webhooks) | #REQUIRED (when not using webhooks) | ||||||
| Login="yourlogin@domain.com" | Login="yourlogin@domain.com" | ||||||
| Password="yourpass" | Password="yourpass" | ||||||
| # When using access token set Login to the User ID associated with your token and Token to your token. |  | ||||||
| # When Token is set Password is ignored. |  | ||||||
| # Login="yOurUSerID" |  | ||||||
| # Token="YoUrUsER_toKEN" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| #### Settings for webhook matterbridge. | #### Settings for webhook matterbridge. | ||||||
| #USE DEDICATED BOT USER WHEN POSSIBLE! This allows you to use advanced features like message editing/deleting and uploads | #USE DEDICATED BOT USER WHEN POSSIBLE! This allows you to use advanced features like message editing/deleting and uploads | ||||||
| @@ -1242,17 +1144,9 @@ Password="yourpass" | |||||||
| #OPTIONAL (default false) | #OPTIONAL (default false) | ||||||
| NoHomeServerSuffix=false | NoHomeServerSuffix=false | ||||||
|  |  | ||||||
| #Whether to disable sending of HTML content to matrix |  | ||||||
| #See https://github.com/42wim/matterbridge/issues/1022 |  | ||||||
| #OPTIONAL (default false) |  | ||||||
| HTMLDisable=false |  | ||||||
|  |  | ||||||
| ## RELOADABLE SETTINGS | ## RELOADABLE SETTINGS | ||||||
| ## Settings below can be reloaded by editing the file | ## Settings below can be reloaded by editing the file | ||||||
|  |  | ||||||
| # UseUserName shows the username instead of the server nickname |  | ||||||
| UseUserName=false |  | ||||||
|  |  | ||||||
| #Whether to prefix messages from other bridges to matrix with the sender's nick.  | #Whether to prefix messages from other bridges to matrix with the sender's nick.  | ||||||
| #Useful if username overrides for incoming webhooks isn't enabled on the  | #Useful if username overrides for incoming webhooks isn't enabled on the  | ||||||
| #matrix server. If you set PrefixMessagesWithNick to true, each message  | #matrix server. If you set PrefixMessagesWithNick to true, each message  | ||||||
| @@ -1415,88 +1309,7 @@ StripNick=false | |||||||
| #OPTIONAL (default false) | #OPTIONAL (default false) | ||||||
| ShowTopicChange=false | ShowTopicChange=false | ||||||
|  |  | ||||||
| ################################################################### |  | ||||||
| # |  | ||||||
| # NCTalk (Nextcloud Talk) |  | ||||||
| # |  | ||||||
| ################################################################### |  | ||||||
|  |  | ||||||
| [nctalk.bridge] |  | ||||||
|  |  | ||||||
| # Url of your Nextcloud server |  | ||||||
| Server = "https://cloud.youdomain.me" |  | ||||||
|  |  | ||||||
| # Enable to not verify the certificate on your Nextcloud server. |  | ||||||
| # e.g. when using selfsigned certificates |  | ||||||
| # OPTIONAL (default false) |  | ||||||
| SkipTLSVerify=true |  | ||||||
|  |  | ||||||
| # Username of the bot |  | ||||||
| Login = "talkuser" |  | ||||||
|  |  | ||||||
| # Password of the bot |  | ||||||
| Password = "talkuserpass" |  | ||||||
|  |  | ||||||
| # Suffix for Guest Users |  | ||||||
| GuestSuffix = " (Guest)" |  | ||||||
|  |  | ||||||
| ################################################################### |  | ||||||
| # |  | ||||||
| # Mumble |  | ||||||
| # |  | ||||||
| ################################################################### |  | ||||||
|  |  | ||||||
| [mumble.bridge] |  | ||||||
|  |  | ||||||
| # Host and port of your Mumble server |  | ||||||
| Server = "mumble.yourdomain.me:64738" |  | ||||||
|  |  | ||||||
| # Nickname to log in as |  | ||||||
| Nick = "matterbridge" |  | ||||||
|  |  | ||||||
| # Some servers require a password |  | ||||||
| # OPTIONAL (default empty) |  | ||||||
| Password = "serverpasswordhere" |  | ||||||
|  |  | ||||||
| # User comment to set on the Mumble user, visible to other users. |  | ||||||
| # OPTIONAL (default empty) |  | ||||||
| UserComment="I am bridging text messages between this channel and #general on irc.yourdomain.me" |  | ||||||
|  |  | ||||||
| # Self-signed TLS client certificate + private key used to connect to |  | ||||||
| # Mumble.  This is required if you want to register the matterbridge |  | ||||||
| # user on your Mumble server, so its nick becomes reserved. |  | ||||||
| # You can generate a keypair using e.g. |  | ||||||
| # |  | ||||||
| #     openssl req -x509 -newkey rsa:2048 -nodes -days 10000 \ |  | ||||||
| #             -keyout mumble.key -out mumble.crt |  | ||||||
| # |  | ||||||
| # To actually register the matterbridege user, connect to Mumble as an |  | ||||||
| # admin, right click on the user and click "Register". |  | ||||||
| # |  | ||||||
| # OPTIONAL (default empty) |  | ||||||
| TLSClientCertificate="mumble.crt" |  | ||||||
| TLSClientKey="mumble.key" |  | ||||||
|  |  | ||||||
| # TLS CA certificate used to validate the Mumble server. |  | ||||||
| # OPTIONAL (defaults to Go system CA) |  | ||||||
| TLSCACertificate=mumble-ca.crt |  | ||||||
|  |  | ||||||
| # Enable to not verify the certificate on your Mumble server. |  | ||||||
| # e.g. when using selfsigned certificates |  | ||||||
| # OPTIONAL (default false) |  | ||||||
| SkipTLSVerify=false |  | ||||||
|  |  | ||||||
| ################################################################### |  | ||||||
| #VK |  | ||||||
| ################################################################### |  | ||||||
| [vk.myvk] |  | ||||||
| #Group access token |  | ||||||
| #See https://vk.com/dev/bots_docs |  | ||||||
| Token="Yourtokenhere" |  | ||||||
|  |  | ||||||
| #Group ID |  | ||||||
| #For example in URL https://vk.com/public168963511 group ID is 168963511 |  | ||||||
| GroupID=123456789 |  | ||||||
|  |  | ||||||
| ################################################################### | ################################################################### | ||||||
| # | # | ||||||
| @@ -1638,8 +1451,6 @@ Buffer=1000 | |||||||
|  |  | ||||||
| #Bearer token used for authentication | #Bearer token used for authentication | ||||||
| #curl -H "Authorization: Bearer token" http://localhost:4242/api/messages | #curl -H "Authorization: Bearer token" http://localhost:4242/api/messages | ||||||
| # https://github.com/vi/websocat |  | ||||||
| # websocat -H="Authorization: Bearer token" ws://127.0.0.1:4242/api/websocket |  | ||||||
| #OPTIONAL (no authorization if token is empty) | #OPTIONAL (no authorization if token is empty) | ||||||
| Token="mytoken" | Token="mytoken" | ||||||
|  |  | ||||||
| @@ -1663,14 +1474,12 @@ RemoteNickFormat="{NICK}" | |||||||
| ## Settings below can be reloaded by editing the file | ## Settings below can be reloaded by editing the file | ||||||
|  |  | ||||||
| #RemoteNickFormat defines how remote users appear on this bridge  | #RemoteNickFormat defines how remote users appear on this bridge  | ||||||
| #The string "{NICK}" (case sensitive) will be replaced by the actual nick. | #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. | ||||||
| #The string "{USERID}" (case sensitive) will be replaced by the user ID. |  | ||||||
| #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge | #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge | ||||||
| #The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge | #The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge | ||||||
| #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge | #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge | ||||||
| #The string "{GATEWAY}" (case sensitive) will be replaced by the origin gateway name that is replicating the message. | #The string "{GATEWAY}" (case sensitive) will be replaced by the origin gateway name that is replicating the message. | ||||||
| #The string "{CHANNEL}" (case sensitive) will be replaced by the origin channel name used by the bridge | #The string "{CHANNEL}" (case sensitive) will be replaced by the origin channel name used by the bridge | ||||||
| #The string "{TENGO}" (case sensitive) will be replaced by the output of the RemoteNickFormat script under [tengo] |  | ||||||
| #OPTIONAL (default empty) | #OPTIONAL (default empty) | ||||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||||
|  |  | ||||||
| @@ -1720,26 +1529,12 @@ MediaDownloadBlacklist=[".html$",".htm$"] | |||||||
| #OPTIONAL (default false) | #OPTIONAL (default false) | ||||||
| IgnoreFailureOnStart=false | IgnoreFailureOnStart=false | ||||||
|  |  | ||||||
| #LogFile defines the location of a file to write logs into, rather |  | ||||||
| #than stdout. |  | ||||||
| #Logging will still happen on stdout if the file cannot be open for |  | ||||||
| #writing, or if the value is empty. Note that the log won't roll, so |  | ||||||
| #you might want to use logrotate(8) with this feature. |  | ||||||
| #OPTIONAL (default empty) |  | ||||||
| LogFile="/var/log/matterbridge.log" |  | ||||||
|  |  | ||||||
| ################################################################### | #TengoModifyMessage allows you to specify the location of a tengo (https://github.com/d5/tengo/) script. | ||||||
| #Tengo configuration |  | ||||||
| ################################################################### |  | ||||||
| #More information about tengo on: https://github.com/d5/tengo/blob/master/docs/tutorial.md and |  | ||||||
| #https://github.com/d5/tengo/blob/master/docs/stdlib.md |  | ||||||
|  |  | ||||||
| [tengo] |  | ||||||
| #InMessage allows you to specify the location of a tengo (https://github.com/d5/tengo/) script. |  | ||||||
| #This script will receive every incoming message and can be used to modify the Username and the Text of that message. | #This script will receive every incoming message and can be used to modify the Username and the Text of that message. | ||||||
| #The script will have the following global variables: | #The script will have the following global variables: | ||||||
| #to modify: msgUsername and msgText | #to modify: msgUsername and msgText | ||||||
| #to read: msgUserID, msgChannel, msgAccount | #to read: msgChannel and msgAccount | ||||||
| # | # | ||||||
| #The script is reloaded on every message, so you can modify the script on the fly. | #The script is reloaded on every message, so you can modify the script on the fly. | ||||||
| # | # | ||||||
| @@ -1752,45 +1547,10 @@ LogFile="/var/log/matterbridge.log" | |||||||
| #    msgText="replaced by this" | #    msgText="replaced by this" | ||||||
| #    msgUsername="fakeuser" | #    msgUsername="fakeuser" | ||||||
| #} | #} | ||||||
|  | #More information about tengo on: https://github.com/d5/tengo/blob/master/docs/tutorial.md and | ||||||
|  | #https://github.com/d5/tengo/blob/master/docs/stdlib.md | ||||||
| #OPTIONAL (default empty) | #OPTIONAL (default empty) | ||||||
| InMessage="example.tengo" | TengoModifyMessage="example.tengo" | ||||||
|  |  | ||||||
| #OutMessage allows you to specify the location of the script that |  | ||||||
| #will be invoked on each message being sent to a bridge and can be used to modify the Username |  | ||||||
| #and the Text of that message. |  | ||||||
| # |  | ||||||
| #The script will have the following global variables: |  | ||||||
| #read-only: |  | ||||||
| #inAccount, inProtocol, inChannel, inGateway, inEvent |  | ||||||
| #outAccount, outProtocol, outChannel, outGateway, outEvent |  | ||||||
| #msgUserID |  | ||||||
| # |  | ||||||
| #read-write: |  | ||||||
| #msgText, msgUsername, msgDrop |  | ||||||
| # |  | ||||||
| #msgDrop is a bool which is default false, when set true this message will be dropped |  | ||||||
| # |  | ||||||
| #The script is reloaded on every message, so you can modify the script on the fly. |  | ||||||
| # |  | ||||||
| #The default script in https://github.com/42wim/matterbridge/tree/master/internal/tengo/outmessage.tengo |  | ||||||
| #is compiled in and will be executed if no script is specified. |  | ||||||
| #OPTIONAL (default empty) |  | ||||||
| OutMessage="example.tengo" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| #RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script. |  | ||||||
| #The script will have the following global variables: |  | ||||||
| #to modify: result |  | ||||||
| #to read: channel, bridge, gateway, protocol, nick, msgUserID |  | ||||||
| # |  | ||||||
| #The result will be set in {TENGO} in the RemoteNickFormat key of every bridge where {TENGO} is specified |  | ||||||
| # |  | ||||||
| #The script is reloaded on every message, so you can modify the script on the fly. |  | ||||||
| # |  | ||||||
| #Example script can be found in https://github.com/42wim/matterbridge/tree/master/contrib/remotenickformat.tengo |  | ||||||
| # |  | ||||||
| #OPTIONAL (default empty) |  | ||||||
| RemoteNickFormat="remotenickformat.tengo" |  | ||||||
|  |  | ||||||
| ################################################################### | ################################################################### | ||||||
| #Gateway configuration | #Gateway configuration | ||||||
| @@ -1821,50 +1581,31 @@ enable=true | |||||||
|     # REQUIRED |     # REQUIRED | ||||||
|     account="irc.freenode" |     account="irc.freenode" | ||||||
|  |  | ||||||
|     # The channel key in each gateway is mapped to a similar group chat ID on the chat platform |     # channel to connect on that account | ||||||
|     # To find the group chat ID for different platforms, refer to the table below |     # How to specify them for the different bridges: | ||||||
|     # |     # | ||||||
|     # Platform   |   Identifier name  |            Example            | Description |     # irc        - #channel (# is required) (this needs to be lowercase!) | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |     # mattermost - channel (the channel name as seen in the URL, not the displayname) | ||||||
|     #            |      channel       |            general            | Do not include the # symbol |     # gitter     - username/room | ||||||
|     #  discord   |    channel id      |          ID:123456789         | See https://github.com/42wim/matterbridge/issues/57 |     # xmpp       - channel | ||||||
|     #            | category/channel   |          Media/gaming         | Without # symbol. If you're using discord categories to group your channels |     # slack      - channel (without the #) | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |     #            - ID:C123456 (where C123456 is the channel ID) does not work with webhook | ||||||
|     #   gitter   |  username/room     |            general            | As seen in the gitter.im URL |     # discord    - channel (without the #) | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |     #            - ID:123456789 (where 123456789 is the channel ID) | ||||||
|     #   hipchat  |    id_channel      |         example needed        | See https://www.hipchat.com/account/xmpp for the correct channel |     #               (https://github.com/42wim/matterbridge/issues/57) | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |     # telegram   - chatid (a large negative number, eg -123456789) | ||||||
|     #    irc     |      channel       |            #general           | The # symbol is required and should be lowercase! |     #             see (https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau) | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |     # hipchat    - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel) | ||||||
|     # mattermost |      channel       |            general            | This is the channel name as seen in the URL, not the display name |     # rocketchat - #channel (# is required (also needed for private channels!) | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |     # matrix     - #channel:server (eg #yourchannel:matrix.org) | ||||||
|     #   matrix   | #channel:server    |    #yourchannel:matrix.org    | Encrypted rooms are not supported in matrix |     #            - encrypted rooms are not supported in matrix | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |     # steam      - chatid (a large number). | ||||||
|     #   msteams  |      threadId      |    19:82abcxx@thread.skype    | You'll find the threadId in the URL |     #             The number in the URL when you click "enter chat room" in the browser | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |     # whatsapp   - 48111222333-123455678999@g.us A unique group JID; | ||||||
|     #   mumble   |    channel id      |              42               | The channel ID, as shown in the channel's "Edit" window |     #              if you specify an empty string bridge will list all the possibilities | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |     #            - "Group Name" if you specify a group name the bridge will hint its JID to specify | ||||||
|     # rocketchat |      channel       |            #channel           | # is required for private channels too |     #              as group names might change in time and contain weird emoticons | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |     # zulip      - stream/topic:topicname (without the #) | ||||||
|     #   slack    |   channel name     |            general            | Do not include the # symbol |  | ||||||
|     #            |    channel id      |           ID:C123456          | The underlying ID of a channel. This doesn't work with webhooks. |  | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |  | ||||||
|     #   steam    |      chatid        |         example needed        | The number in the URL when you click "enter chat room" in the browser |  | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |  | ||||||
|     #   nctalk   |      token         |           xs25tz5y            | The token in the URL when you are in a chat. It will be the last part of the URL. |  | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |  | ||||||
|     #  telegram  |      chatid        |          -123456789           | A large negative number. see https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau |  | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |  | ||||||
|     #  vk        |      peerid        |          2000000002           | A number that starts form 2000000000. Use --debug and send any message in chat to get PeerID in the logs  |  | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |  | ||||||
|     #  whatsapp  |     group JID      | 48111222333-123455678999@g.us | A unique group JID. If you specify an empty string, bridge will list all the possibilities |  | ||||||
|     #            |    "Group Name"    |         "Family Chat"         | if you specify a group name, the bridge will find hint the JID to specify. Names can change over time and are not stable. |  | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |  | ||||||
|     #    xmpp    |      channel       |            general            | The room name |  | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |  | ||||||
|     #   zulip    | stream/topic:topic |     general/off-topic:food    | Do not use the # when specifying a topic |  | ||||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- |  | ||||||
|  |  | ||||||
|     #                   |     #                   | ||||||
|     # REQUIRED |     # REQUIRED | ||||||
|     channel="#testing" |     channel="#testing" | ||||||
| @@ -1896,16 +1637,13 @@ enable=true | |||||||
|         #OPTIONAL - your irc / xmpp channel key |         #OPTIONAL - your irc / xmpp channel key | ||||||
|         key="yourkey" |         key="yourkey" | ||||||
|  |  | ||||||
|     # Discord specific gateway options |  | ||||||
|     [[gateway.inout]] |     [[gateway.inout]] | ||||||
|     account="discord.game" |     account="discord.game" | ||||||
|     channel="mygreatgame" |     channel="mygreatgame" | ||||||
|  |  | ||||||
|  |         #OPTIONAL - webhookurl only works for discord (it needs a different URL for each cahnnel) | ||||||
|         [gateway.inout.options] |         [gateway.inout.options] | ||||||
|         # WebhookURL sends messages in the style of "puppets". You must configure a webhook URL for each channel you want to bridge. |         webhookurl="https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y" | ||||||
|         # If you have more than one channel and don't wnat to configure each channel manually, see the "AutoWebhooks" option in the gateway config. |  | ||||||
|         # Example: "https://discord.com/api/webhooks/1234/abcd_xyzw" |  | ||||||
|         WebhookURL="" |  | ||||||
|  |  | ||||||
|     [[gateway.inout]] |     [[gateway.inout]] | ||||||
|     account="zulip.streamchat" |     account="zulip.streamchat" | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/mattermost/mattermost-server/v5/model" | 	"github.com/mattermost/mattermost-server/model" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // GetChannels returns all channels we're members off | // GetChannels returns all channels we're members off | ||||||
| @@ -36,16 +36,6 @@ func (m *MMClient) GetChannelHeader(channelId string) string { //nolint:golint | |||||||
| 	return "" | 	return "" | ||||||
| } | } | ||||||
|  |  | ||||||
| func getNormalisedName(channel *model.Channel) string { |  | ||||||
| 	if channel.Type == model.CHANNEL_GROUP { |  | ||||||
| 		// (deprecated in favor of ReplaceAll in go 1.12) |  | ||||||
| 		res := strings.Replace(channel.DisplayName, ", ", "-", -1) //nolint: gocritic |  | ||||||
| 		res = strings.Replace(res, " ", "_", -1)                   //nolint: gocritic |  | ||||||
| 		return res |  | ||||||
| 	} |  | ||||||
| 	return channel.Name |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint | func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint | ||||||
| 	m.RLock() | 	m.RLock() | ||||||
| 	defer m.RUnlock() | 	defer m.RUnlock() | ||||||
| @@ -55,7 +45,13 @@ func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:go | |||||||
|  |  | ||||||
| 	for _, t := range m.OtherTeams { | 	for _, t := range m.OtherTeams { | ||||||
| 		for _, channel := range append(t.Channels, t.MoreChannels...) { | 		for _, channel := range append(t.Channels, t.MoreChannels...) { | ||||||
| 			if getNormalisedName(channel) == name { | 			if channel.Type == model.CHANNEL_GROUP { | ||||||
|  | 				res := strings.Replace(channel.DisplayName, ", ", "-", -1) | ||||||
|  | 				res = strings.Replace(res, " ", "_", -1) | ||||||
|  | 				if res == name { | ||||||
|  | 					return channel.Id | ||||||
|  | 				} | ||||||
|  | 			} else if channel.Name == name { | ||||||
| 				return channel.Id | 				return channel.Id | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -67,7 +63,7 @@ func (m *MMClient) getChannelIdTeam(name string, teamId string) string { //nolin | |||||||
| 	for _, t := range m.OtherTeams { | 	for _, t := range m.OtherTeams { | ||||||
| 		if t.Id == teamId { | 		if t.Id == teamId { | ||||||
| 			for _, channel := range append(t.Channels, t.MoreChannels...) { | 			for _, channel := range append(t.Channels, t.MoreChannels...) { | ||||||
| 				if getNormalisedName(channel) == name { | 				if channel.Name == name { | ||||||
| 					return channel.Id | 					return channel.Id | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @@ -85,7 +81,12 @@ func (m *MMClient) GetChannelName(channelId string) string { //nolint:golint | |||||||
| 		} | 		} | ||||||
| 		for _, channel := range append(t.Channels, t.MoreChannels...) { | 		for _, channel := range append(t.Channels, t.MoreChannels...) { | ||||||
| 			if channel.Id == channelId { | 			if channel.Id == channelId { | ||||||
| 				return getNormalisedName(channel) | 				if channel.Type == model.CHANNEL_GROUP { | ||||||
|  | 					res := strings.Replace(channel.DisplayName, ", ", "-", -1) | ||||||
|  | 					res = strings.Replace(res, " ", "_", -1) | ||||||
|  | 					return res | ||||||
|  | 				} | ||||||
|  | 				return channel.Name | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -166,42 +167,23 @@ func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *MMClient) UpdateChannelsTeam(teamID string) error { |  | ||||||
| 	mmchannels, resp := m.Client.GetChannelsForTeamForUser(teamID, m.User.Id, false, "") |  | ||||||
| 	if resp.Error != nil { |  | ||||||
| 		return errors.New(resp.Error.DetailedError) |  | ||||||
| 	} |  | ||||||
| 	for idx, t := range m.OtherTeams { |  | ||||||
| 		if t.Id == teamID { |  | ||||||
| 			m.Lock() |  | ||||||
| 			m.OtherTeams[idx].Channels = mmchannels |  | ||||||
| 			m.Unlock() |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	mmchannels, resp = m.Client.GetPublicChannelsForTeam(teamID, 0, 5000, "") |  | ||||||
| 	if resp.Error != nil { |  | ||||||
| 		return errors.New(resp.Error.DetailedError) |  | ||||||
| 	} |  | ||||||
| 	for idx, t := range m.OtherTeams { |  | ||||||
| 		if t.Id == teamID { |  | ||||||
| 			m.Lock() |  | ||||||
| 			m.OtherTeams[idx].MoreChannels = mmchannels |  | ||||||
| 			m.Unlock() |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *MMClient) UpdateChannels() error { | func (m *MMClient) UpdateChannels() error { | ||||||
| 	if err := m.UpdateChannelsTeam(m.Team.Id); err != nil { | 	mmchannels, resp := m.Client.GetChannelsForTeamForUser(m.Team.Id, m.User.Id, "") | ||||||
| 		return err | 	if resp.Error != nil { | ||||||
|  | 		return errors.New(resp.Error.DetailedError) | ||||||
| 	} | 	} | ||||||
| 	for _, t := range m.OtherTeams { | 	m.Lock() | ||||||
| 		if err := m.UpdateChannelsTeam(t.Id); err != nil { | 	m.Team.Channels = mmchannels | ||||||
| 			return err | 	m.Unlock() | ||||||
| 		} |  | ||||||
|  | 	mmchannels, resp = m.Client.GetPublicChannelsForTeam(m.Team.Id, 0, 5000, "") | ||||||
|  | 	if resp.Error != nil { | ||||||
|  | 		return errors.New(resp.Error.DetailedError) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	m.Lock() | ||||||
|  | 	m.Team.MoreChannels = mmchannels | ||||||
|  | 	m.Unlock() | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/gorilla/websocket" | 	"github.com/gorilla/websocket" | ||||||
| 	"github.com/jpillora/backoff" | 	"github.com/jpillora/backoff" | ||||||
| 	"github.com/mattermost/mattermost-server/v5/model" | 	"github.com/mattermost/mattermost-server/model" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error { | func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error { | ||||||
| @@ -154,7 +154,7 @@ func (m *MMClient) initUser() error { | |||||||
|  |  | ||||||
| 		t := &Team{Team: team, Users: usermap, Id: team.Id} | 		t := &Team{Team: team, Users: usermap, Id: team.Id} | ||||||
|  |  | ||||||
| 		mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, false, "") | 		mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, "") | ||||||
| 		if resp.Error != nil { | 		if resp.Error != nil { | ||||||
| 			return resp.Error | 			return resp.Error | ||||||
| 		} | 		} | ||||||
| @@ -186,19 +186,15 @@ func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error { | |||||||
| 		if resp.Error != nil { | 		if resp.Error != nil { | ||||||
| 			return fmt.Errorf("%#v", resp.Error.Error()) | 			return fmt.Errorf("%#v", resp.Error.Error()) | ||||||
| 		} | 		} | ||||||
| 		if firstConnection && !m.SkipVersionCheck && !supportedVersion(resp.ServerVersion) { | 		if firstConnection && !supportedVersion(resp.ServerVersion) { | ||||||
| 			return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion) | 			return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion) | ||||||
| 		} | 		} | ||||||
| 		if !m.SkipVersionCheck { | 		m.ServerVersion = resp.ServerVersion | ||||||
| 			m.ServerVersion = resp.ServerVersion | 		if m.ServerVersion == "" { | ||||||
| 			if m.ServerVersion == "" { | 			m.logger.Debugf("Server not up yet, reconnecting in %s", d) | ||||||
| 				m.logger.Debugf("Server not up yet, reconnecting in %s", d) | 			time.Sleep(d) | ||||||
| 				time.Sleep(d) |  | ||||||
| 			} else { |  | ||||||
| 				m.logger.Infof("Found version %s", m.ServerVersion) |  | ||||||
| 				return nil |  | ||||||
| 			} |  | ||||||
| 		} else { | 		} else { | ||||||
|  | 			m.logger.Infof("Found version %s", m.ServerVersion) | ||||||
| 			return nil | 			return nil | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user