Compare commits
	
		
			275 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b74b884793 | |||
|   | c4157a4d5b | ||
|   | d16645c952 | ||
|   | 65d78e38af | ||
|   | a89267943c | ||
|   | 815d8b804f | ||
|   | 6edd5de3b7 | ||
|   | 2e9db32a83 | ||
|   | 4bf1c0450c | ||
|   | 0bb521512a | ||
|   | 13fd5c5d5b | ||
|   | d055b4530e | ||
|   | 6b528ffa4f | ||
|   | fa147c076f | ||
|   | b2df32bc81 | ||
|   | 733f4c71b8 | ||
|   | 70e8c6e9d3 | ||
|   | 2f33fe86f5 | ||
|   | 56e7bd01ca | ||
|   | 9459495484 | ||
|   | 89b0d362d2 | ||
|   | 574f25337d | ||
|   | 5bbe422161 | ||
|   | 6500714a93 | ||
|   | 5feafcddba | ||
|   | 3e20a3d180 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 60639b6e50 | ||
|   | 839f384e45 | ||
|   | d42277979a | ||
|   | 24cf007a74 | ||
|   | 601f48a50e | ||
|   | c2b8e298d8 | ||
|   | 0917e17383 | ||
|   | 8587fa8585 | ||
|   | f345eeae55 | ||
|   | 89e2dbac15 | ||
|   | 356ada872c | ||
|   | a3deb48726 | ||
|   | 544cd5cd9e | ||
|   | 53b63adc71 | ||
|   | 2c349c50c7 | ||
|   | 08779c2909 | ||
|   | d5f9cdf912 | ||
|   | 3a4bdd7c56 | ||
|   | 2b236f3ff7 | ||
|   | 768fb791c5 | ||
|   | d00dcf3f58 | ||
|   | d5980303e5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f436ae7b31 | ||
|   | a9ccc18d73 | ||
|   | ac681687f8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 57ce19150f | ||
|   | 24f6747516 | ||
|   | 6dafebc7cc | ||
|   | d23f2560d5 | ||
|   | 880586bac4 | ||
|   | eac2a8c8dc | ||
|   | 4cf313c4c6 | ||
|   | 91353d0a4d | ||
|   | 0a6d64ab48 | ||
|   | 4fd0a76727 | ||
|   | 6da9d567dc | ||
|   | 6d5a3dff22 | ||
|   | 3ad5deaff1 | ||
|   | 9bbdf70e69 | ||
|   | 0c83946983 | ||
|   | fda05f2262 | ||
|   | 7abf1a5884 | ||
|   | 365acc36ea | ||
|   | 0482cd191d | ||
|   | 6a3fc71397 | ||
|   | 3c4192ebf6 | ||
|   | e450e1c447 | ||
|   | 20f841c513 | ||
|   | d07a3e09c9 | ||
|   | 4649876956 | ||
|   | 5604d140e3 | ||
|   | 8751fb4bb1 | ||
|   | 3819062574 | ||
|   | 051e6e76e9 | ||
|   | 1e55dd47f2 | ||
|   | 700b95546b | ||
|   | 2fa96ec0ed | ||
|   | 81e6f75aa4 | ||
|   | 888c8b9a84 | ||
|   | e775a8a22e | ||
|   | 99fbd9cae6 | ||
|   | 67adad3e08 | ||
|   | 2fca3c7563 | ||
|   | c3573f1a46 | ||
|   | ee932a9f8e | ||
|   | ce18c948e6 | ||
|   | 7bc93c5506 | ||
|   | d7cad3b404 | ||
|   | 7740a362c9 | ||
|   | 281ef53e7d | ||
|   | f044b948e2 | ||
|   | 32474a5f4d | ||
|   | 26596acf80 | ||
|   | e63870a631 | ||
|   | ce782ff6fb | ||
|   | c6716e030c | ||
|   | 4ab72acec6 | ||
|   | 30aae8e257 | ||
|   | d7b7ff7bb4 | ||
|   | 6fe0cff342 | ||
|   | 5f75f9886d | ||
|   | 5d9604cd15 | ||
|   | cc36ebf1c9 | ||
|   | e6adecfd81 | ||
|   | 5c8f224e3b | ||
|   | 952221d3b9 | ||
|   | 496d5b4ec7 | ||
|   | 2623a412c4 | ||
|   | d64eed49bc | ||
|   | fffa29c2f3 | ||
|   | 4da1444ffc | ||
|   | 21c4e56d16 | ||
|   | 5356b3856a | ||
|   | 320c996a21 | ||
|   | 69c74be7bb | ||
|   | aefa70891c | ||
|   | 1b9877fda4 | ||
|   | 0205a67309 | ||
|   | e3cafeaf92 | ||
|   | e7b193788a | ||
|   | 17da95b094 | ||
|   | c5e49eec96 | ||
|   | 24bc0f127b | ||
|   | f0f801402d | ||
|   | 663850a2b8 | ||
|   | c51753cab1 | ||
|   | b3be2e208c | ||
|   | c30e90ff3f | ||
|   | e4c0ca0f48 | ||
|   | 9c203327c0 | ||
|   | ccb5b1d075 | ||
|   | 0dbbd0414c | ||
|   | e7b3ebf98a | ||
|   | 5bc18fb780 | ||
|   | df30366072 | ||
|   | 65c7ac80b5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | dd3fb32ec7 | ||
|   | 2a3f475ff5 | ||
|   | 7288f71201 | ||
|   | 9c43eff753 | ||
|   | c8d7fdeedc | ||
|   | c211152e23 | ||
|   | ab75d5097e | ||
|   | c3644c8d3b | ||
|   | 6438a3dba3 | ||
|   | 4b226a6a63 | ||
|   | 4801850013 | ||
|   | 6a7412bf2b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 5a1fd7dadd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ac06a26809 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 61d56f26f8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6aa05b3981 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | aad60c882e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | fecca57507 | ||
|   | 2bcad846c0 | ||
|   | 15ad0165fc | ||
|   | 2e8ab11978 | ||
|   | 9a8ce9b17e | ||
|   | 16ab4c6fed | ||
|   | e3ee0df7ba | ||
|   | 8f7ab280e2 | ||
|   | dbedc99421 | ||
|   | 6cb359cb80 | ||
|   | ae2ad824a9 | ||
|   | 02e3d7852b | ||
|   | 3893a035be | ||
|   | 658bdd9faa | ||
|   | e1eebcd4e0 | ||
|   | 062b831e88 | ||
|   | b275efaeff | ||
|   | 80d3033456 | ||
|   | bd0516f09a | ||
|   | df4d76e466 | ||
|   | dcbd7f8cad | ||
|   | 73ec02ab9d | ||
|   | d1f8347071 | ||
|   | 8601eedada | ||
|   | 9afd33cdfc | ||
|   | 5e1be8e558 | ||
|   | 835dd2635a | ||
|   | f65b18c2f6 | ||
|   | b0e7b84f40 | ||
|   | 1635db93c7 | ||
|   | c4fe462d11 | ||
|   | b1f403165d | ||
|   | 46e4317b77 | ||
|   | e3ffbcadd8 | ||
|   | b7d73077e5 | ||
|   | 77f61ee20a | ||
|   | 8967f02fc9 | ||
|   | 831ff6d0a9 | ||
|   | 2199174def | ||
|   | 55f41ddaab | ||
|   | 21305d93bf | ||
|   | 4478d5d904 | ||
|   | cc6253a6b8 | ||
|   | 85f66853bc | ||
|   | 7464fd149c | ||
|   | 86f1a8019c | ||
|   | b98d56dcf6 | ||
|   | a3a8a5769d | ||
|   | 4dd8bae5c9 | ||
|   | 7ae45c42e7 | ||
|   | 7551b4e7a3 | ||
|   | 61bab22dde | ||
|   | 6dcc23ebb6 | ||
|   | b06a574cc5 | ||
|   | b56f80b1b8 | ||
|   | 20f6c05ec5 | ||
|   | 57fce93af7 | ||
|   | 110b6a1431 | ||
|   | 53cafa9f3d | ||
|   | d4195deb3a | ||
|   | 400ecfb79c | ||
|   | 86151da271 | ||
|   | 44f3e2557d | ||
|   | 1f365c716e | ||
|   | 9efcc41ab2 | ||
|   | 13bbeeaceb | ||
|   | da4dcec14d | ||
|   | 761c0b79c5 | ||
|   | d93ab0496f | ||
|   | 66b6f9749d | ||
|   | 17c2d1f26a | ||
|   | a79e632cdc | ||
|   | f36498421b | ||
|   | e45bbe4571 | ||
|   | fb5a84212c | ||
|   | dedc1c45a1 | ||
|   | 6a12f9ff84 | ||
|   | 641ed1873b | ||
|   | 1d50da4b1c | ||
|   | c7897cca5d | ||
|   | 4091b6f6b4 | ||
|   | 766f35554e | ||
|   | c86137449e | ||
|   | efec01a92f | ||
|   | 4fcad8e04b | ||
|   | 4b4b2d790e | ||
|   | ec6ae343dd | ||
|   | b9fb361959 | ||
|   | a189298ab0 | ||
|   | 714a2ad730 | ||
|   | fa8b96dfa1 | ||
|   | 01955a0df8 | ||
|   | ac4aee39e3 | ||
|   | a0bca42a7a | ||
|   | af543dcd05 | ||
|   | af77109a47 | ||
|   | b979aff270 | ||
|   | b293e3fa75 | ||
|   | 21eb37e471 | ||
|   | d3b60cc445 | ||
|   | 7466e1d014 | ||
|   | 2a7f28606c | ||
|   | 0450482e6e | ||
|   | ee5d9b43b5 | ||
|   | 3a8857c8c9 | ||
|   | be3dfb251d | ||
|   | 4e11e29f70 | ||
|   | 763bb95cea | ||
|   | 668e7407e6 | ||
|   | c147ba1da1 | ||
|   | 10f044c3dd | ||
|   | ce5140febd | ||
|   | 858cdc86f5 | ||
|   | 9a25297d51 | ||
|   | e24f7f5151 | ||
|   | eff5f1e119 | 
							
								
								
									
										33
									
								
								.github/workflows/development.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -5,27 +5,28 @@ jobs: | ||||
|     name: golangci-lint | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/checkout@v3 | ||||
|         with: | ||||
|           fetch-depth: 20 | ||||
|       - name: Run golangci-lint | ||||
|         uses: golangci/golangci-lint-action@v2 | ||||
|         uses: golangci/golangci-lint-action@v3 | ||||
|         with: | ||||
|           version: v1.29 | ||||
|           args: "-v --new-from-rev HEAD~5" | ||||
|           version: latest | ||||
|           args: "-v --new-from-rev HEAD~5 --timeout=5m" | ||||
|   test-build-upload: | ||||
|     strategy: | ||||
|       matrix: | ||||
|         go-version: [1.14.x, 1.15.x] | ||||
|         go-version: [1.22.x] | ||||
|         platform: [ubuntu-latest] | ||||
|     runs-on: ${{ matrix.platform }} | ||||
|     steps: | ||||
|     - name: Install Go | ||||
|       uses: actions/setup-go@v2 | ||||
|       uses: actions/setup-go@v3 | ||||
|       with: | ||||
|         go-version: ${{ matrix.go-version }} | ||||
|         stable: false | ||||
|     - name: Checkout code | ||||
|       uses: actions/checkout@v2 | ||||
|       uses: actions/checkout@v3 | ||||
|       with: | ||||
|           fetch-depth: 0 | ||||
|     - name: Test | ||||
| @@ -34,24 +35,24 @@ jobs: | ||||
|       run: | | ||||
|         mkdir -p output/{win,lin,arm,mac} | ||||
|         VERSION=$(git describe --tags) | ||||
|         GOOS=linux GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64 | ||||
|         GOOS=windows GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe | ||||
|         GOOS=darwin GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64 | ||||
|         CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64 | ||||
|         CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe | ||||
|         CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64 | ||||
|     - name: Upload linux 64-bit | ||||
|       if: startsWith(matrix.go-version,'1.15') | ||||
|       uses: actions/upload-artifact@v2 | ||||
|       if: startsWith(matrix.go-version,'1.22') | ||||
|       uses: actions/upload-artifact@v3 | ||||
|       with: | ||||
|         name: matterbridge-linux-64bit | ||||
|         path: output/lin | ||||
|     - name: Upload windows 64-bit | ||||
|       if: startsWith(matrix.go-version,'1.15') | ||||
|       uses: actions/upload-artifact@v2 | ||||
|       if: startsWith(matrix.go-version,'1.22') | ||||
|       uses: actions/upload-artifact@v3 | ||||
|       with: | ||||
|         name: matterbridge-windows-64bit | ||||
|         path: output/win | ||||
|     - name: Upload darwin 64-bit | ||||
|       if: startsWith(matrix.go-version,'1.15') | ||||
|       uses: actions/upload-artifact@v2 | ||||
|       if: startsWith(matrix.go-version,'1.22') | ||||
|       uses: actions/upload-artifact@v3 | ||||
|       with: | ||||
|         name: matterbridge-darwin-64bit | ||||
|         path: output/mac | ||||
|   | ||||
							
								
								
									
										68
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,68 @@ | ||||
| name: docker | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - 'master' | ||||
|     tags: | ||||
|       - 'v*' | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - 'master' | ||||
|  | ||||
| jobs: | ||||
|   docker: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|       - | ||||
|         name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v1 | ||||
|         with: | ||||
|           platforms: amd64,arm64 | ||||
|       - | ||||
|         name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v1 | ||||
|       - | ||||
|         name: Docker meta | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v3 | ||||
|         with: | ||||
|           images: 42wim/matterbridge,ghcr.io/42wim/matterbridge | ||||
|           flavor: | | ||||
|             latest=true | ||||
|           tags: | | ||||
|             type=ref,event=branch | ||||
|             type=ref,event=pr | ||||
|             type=semver,pattern={{version}} | ||||
|             type=semver,pattern=stable | ||||
|             type=semver,pattern={{major}} | ||||
|             type=semver,pattern={{major}}.{{minor}} | ||||
|       - | ||||
|         name: Login to DockerHub | ||||
|         uses: docker/login-action@v1  | ||||
|         if: github.event_name != 'pull_request' | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|       - | ||||
|         name: Log into registry ghcr.io | ||||
|         uses: docker/login-action@v1 | ||||
|         if: github.event_name != 'pull_request' | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|       - | ||||
|         name: Build and push | ||||
|         id: docker_build | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           context: . | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|           push: ${{ github.event_name != 'pull_request' }} | ||||
|           tags: ${{ steps.meta.outputs.tags }} | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|  | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -4,3 +4,6 @@ | ||||
|  | ||||
| # Exclude configuration file | ||||
| matterbridge.toml | ||||
|  | ||||
| # Exclude IDE Files | ||||
| .vscode | ||||
|   | ||||
| @@ -7,7 +7,7 @@ run: | ||||
|   # concurrency: 4 | ||||
|  | ||||
|   # timeout for analysis, e.g. 30s, 5m, default is 1m | ||||
|   deadline: 2m | ||||
|   deadline: 5m | ||||
|  | ||||
|   # exit code when at least one issue was found, default is 1 | ||||
|   issues-exit-code: 1 | ||||
| @@ -182,7 +182,39 @@ linters: | ||||
|     - interfacer | ||||
|     - goheader | ||||
|     - noctx | ||||
|  | ||||
|     - gci | ||||
|     - errorlint | ||||
|     - nlreturn | ||||
|     - exhaustivestruct | ||||
|     - forbidigo | ||||
|     - wrapcheck | ||||
|     - varnamelen | ||||
|     - ireturn | ||||
|     - errorlint | ||||
|     - tparallel | ||||
|     - wrapcheck | ||||
|     - paralleltest | ||||
|     - makezero | ||||
|     - thelper | ||||
|     - cyclop | ||||
|     - revive | ||||
|     - importas | ||||
|     - gomoddirectives | ||||
|     - promlinter | ||||
|     - tagliatelle | ||||
|     - errname | ||||
|     - typecheck | ||||
|     - grouper | ||||
|     - decorder | ||||
|     - maintidx | ||||
|     - exhaustruct | ||||
|     - asasalint | ||||
|     - execinquery | ||||
|     - nosnakecase | ||||
|     - exhaustive | ||||
|     - testifylint | ||||
|     - mnd | ||||
|     - depguard | ||||
| # rules to deal with reported isues | ||||
| issues: | ||||
|   # List of regexps of issue texts to exclude, empty list by default. | ||||
|   | ||||
| @@ -18,8 +18,11 @@ builds: | ||||
|     - arm | ||||
|     - arm64 | ||||
|     - 386 | ||||
|   goarm: | ||||
|     - 6 | ||||
|     - 7 | ||||
|   ldflags: | ||||
|     - -s -w -X main.githash={{.ShortCommit}} | ||||
|     - -s -w -X github.com/42wim/matterbridge/version.GitHash={{.ShortCommit}} | ||||
|  | ||||
| archives: | ||||
|   - | ||||
|   | ||||
| @@ -3,7 +3,7 @@ FROM alpine AS builder | ||||
| COPY . /go/src/matterbridge | ||||
| RUN apk --no-cache add go git \ | ||||
|         && cd /go/src/matterbridge \ | ||||
|         && go build -mod vendor -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge | ||||
|         && CGO_ENABLED=0 go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge | ||||
|  | ||||
| FROM alpine | ||||
| RUN apk --no-cache add ca-certificates mailcap | ||||
|   | ||||
							
								
								
									
										14
									
								
								Dockerfile_whatsappmulti
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| FROM alpine AS builder | ||||
|  | ||||
| COPY . /go/src/matterbridge | ||||
| RUN apk --no-cache add go git \ | ||||
|         && cd /go/src/matterbridge \ | ||||
|         && CGO_ENABLED=0 go build -tags whatsappmulti -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge | ||||
|  | ||||
| FROM alpine | ||||
| RUN apk --no-cache add ca-certificates mailcap | ||||
| COPY --from=builder /bin/matterbridge /bin/matterbridge | ||||
| RUN mkdir /etc/matterbridge \ | ||||
|   && touch /etc/matterbridge/matterbridge.toml \ | ||||
|   && ln -sf /matterbridge.toml /etc/matterbridge/matterbridge.toml | ||||
| ENTRYPOINT ["/bin/matterbridge", "-conf", "/etc/matterbridge/matterbridge.toml"] | ||||
							
								
								
									
										120
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -58,20 +58,22 @@ And more... | ||||
|     - [Binaries](#binaries) | ||||
|     - [Packages](#packages) | ||||
|   - [Building](#building) | ||||
|   - [Building with whatsapp (beta) multidevice support](#building-with-whatsapp-beta-multidevice-support) | ||||
|   - [Configuration](#configuration) | ||||
|     - [Basic configuration](#basic-configuration) | ||||
|     - [Settings](#settings) | ||||
|     - [Advanced configuration](#advanced-configuration) | ||||
|     - [Examples](#examples) | ||||
|       - [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing) | ||||
|       - [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) | ||||
|       - [Examples](#examples) | ||||
|         - [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing) | ||||
|         - [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general) | ||||
|     - [Running](#running) | ||||
|       - [Docker](#docker) | ||||
|       - [Systemd](#systemd) | ||||
|     - [Changelog](#changelog) | ||||
|     - [FAQ](#faq) | ||||
|     - [Related projects](#related-projects) | ||||
|     - [Articles / Tutorials](#articles--tutorials) | ||||
|     - [Thanks](#thanks) | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| @@ -88,6 +90,7 @@ And more... | ||||
|  | ||||
| - [Discord](https://discordapp.com) | ||||
| - [Gitter](https://gitter.im) | ||||
| - [Harmony](https://harmonyapp.io) | ||||
| - [IRC](http://www.mirc.com/servers.html) | ||||
| - [Keybase](https://keybase.io) | ||||
| - [Matrix](https://matrix.org) | ||||
| @@ -104,19 +107,28 @@ And more... | ||||
| - [Twitch](https://twitch.tv) | ||||
| - [VK](https://vk.com/) | ||||
| - [WhatsApp](https://www.whatsapp.com/) | ||||
|   - Whatsapp legacy is natively supported | ||||
|   - Whatsapp multidevice beta is natively supported but you need to build yourself, see [here](#building-with-whatsapp-beta-multidevice-support) | ||||
| - [XMPP](https://xmpp.org) | ||||
| - [Zulip](https://zulipchat.com) | ||||
|  | ||||
| ### 3rd party via matterbridge api | ||||
|  | ||||
| - [Delta Chat](https://github.com/deltachat-bot/matterdelta) | ||||
| - [Minecraft](https://github.com/raws/mattercraft) | ||||
| - [Minecraft](https://gitlab.com/Programie/MatterBukkit) | ||||
|  | ||||
| #### Past 3rd party projects | ||||
| - [Discourse](https://github.com/DeclanHoare/matterbabble) | ||||
| - [Facebook messenger](https://github.com/powerjungle/fbridge-asyncio) | ||||
| - [Facebook messenger](https://github.com/VictorNine/fbridge) | ||||
| - [Minecraft](https://github.com/elytra/MatterLink) | ||||
| - [Minecraft](https://github.com/raws/mattercraft) | ||||
| - [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) | ||||
| - [MatterAMXX](https://github.com/andrewlindberg/MatterAMXX): [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430) | ||||
| - [Vintage Story](https://github.com/NikkyAI/vs-matterbridge) | ||||
| - [Ultima Online Emulator](https://github.com/kuoushi/ServUO-Matterbridge) | ||||
| - [Teamspeak](https://github.com/Archeb/ts-matterbridge) | ||||
|  | ||||
|  | ||||
| ### API | ||||
|  | ||||
| @@ -125,14 +137,19 @@ More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/ | ||||
|  | ||||
| Used by the projects below. Feel free to make a PR to add your project to this list. | ||||
|  | ||||
| - [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat) | ||||
| - [Minecraft](https://github.com/raws/mattercraft) (Matterbridge link for Minecraft Server chat) | ||||
| - [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Forge server chat, archived) | ||||
| - [MatterCraft](https://github.com/raws/mattercraft) (Matterbridge link for Minecraft Forge server chat) | ||||
| - [MatterBukkit](https://gitlab.com/Programie/MatterBukkit) (Matterbridge link for Minecraft Bukkit/Spigot server chat) | ||||
| - [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot) | ||||
| - [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat 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) | ||||
| - [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers) | ||||
| - [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge) | ||||
| - [beerchat](https://github.com/mt-mods/beerchat) (Matterbridge link for minetest) | ||||
|  | ||||
| ## Chat with us | ||||
|  | ||||
| @@ -159,25 +176,71 @@ See <https://github.com/42wim/matterbridge/wiki> | ||||
|  | ||||
| ### Binaries | ||||
|  | ||||
| - Latest stable release [v1.22.0](https://github.com/42wim/matterbridge/releases/latest) | ||||
| - Latest stable release [v1.26.0](https://github.com/42wim/matterbridge/releases/latest) | ||||
| - 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. | ||||
| 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.24.1-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 | ||||
|  | ||||
| - [Overview](https://repology.org/metapackage/matterbridge/versions) | ||||
| - [snap](https://snapcraft.io/matterbridge) | ||||
| - [scoop](https://github.com/42wim/scoop-bucket) | ||||
|  | ||||
| ## Building | ||||
|  | ||||
| Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest) | ||||
|  | ||||
| If you really want to build from source, follow these instructions: | ||||
| Go 1.13+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed. | ||||
| Go 1.18+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed. | ||||
|  | ||||
| Building the binary with **all** the bridges enabled needs about 3GB RAM to compile. | ||||
| You can reduce this memory requirement to 0,5GB RAM by adding the `nomsteams` tag if you don't need/use the Microsoft Teams bridge. | ||||
|  | ||||
| Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`) | ||||
|  | ||||
| To install the latest stable run: | ||||
|  | ||||
| ```bash | ||||
| go get github.com/42wim/matterbridge | ||||
| go install github.com/42wim/matterbridge | ||||
| ``` | ||||
|  | ||||
| To install the latest dev run: | ||||
|  | ||||
| ```bash | ||||
| go install github.com/42wim/matterbridge@master | ||||
| ``` | ||||
|  | ||||
| To install the latest stable run without msteams or zulip bridge: | ||||
|  | ||||
| ```bash | ||||
| go install -tags nomsteams,nozulip github.com/42wim/matterbridge | ||||
| ``` | ||||
|  | ||||
| You should now have matterbridge binary in the ~/go/bin directory: | ||||
|  | ||||
| ```bash | ||||
| $ ls ~/go/bin/ | ||||
| matterbridge | ||||
| ``` | ||||
|  | ||||
| ## Building with whatsapp (beta) multidevice support | ||||
|  | ||||
| Because the library we use for Whatsapp multidevice support includes a GPL3 library we can not provide you binaries. | ||||
| (as this would require the Matterbridge to change it license to GPL) | ||||
|  | ||||
| Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`) | ||||
|  | ||||
| So this means you have to build it yourself using the instructions below: | ||||
|  | ||||
| ```bash | ||||
| go install -tags whatsappmulti github.com/42wim/matterbridge@master | ||||
| ``` | ||||
|  | ||||
| If you're low on memory and don't need msteams: | ||||
|  | ||||
| ```bash | ||||
| go install -tags nomsteams,whatsappmulti github.com/42wim/matterbridge@master | ||||
| ``` | ||||
|  | ||||
| You should now have matterbridge binary in the ~/go/bin directory: | ||||
| @@ -207,8 +270,8 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for | ||||
|  | ||||
| ```toml | ||||
| [irc] | ||||
|     [irc.freenode] | ||||
|     Server="irc.freenode.net:6667" | ||||
|     [irc.libera] | ||||
|     Server="irc.libera.chat:6667" | ||||
|     Nick="yourbotname" | ||||
|  | ||||
| [mattermost] | ||||
| @@ -224,7 +287,7 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for | ||||
| name="mygateway" | ||||
| enable=true | ||||
|     [[gateway.inout]] | ||||
|     account="irc.freenode" | ||||
|     account="irc.libera" | ||||
|     channel="#testing" | ||||
|  | ||||
|     [[gateway.inout]] | ||||
| @@ -281,6 +344,10 @@ Usage of ./matterbridge: | ||||
|  | ||||
| Please take a look at the [Docker Wiki page](https://github.com/42wim/matterbridge/wiki/Deploy:-Docker) for more information. | ||||
|  | ||||
| ### Systemd | ||||
|  | ||||
| Please take a look at the [Service Files page](https://github.com/42wim/matterbridge/wiki/Service-files) for more information. | ||||
|  | ||||
| ## Changelog | ||||
|  | ||||
| See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md) | ||||
| @@ -305,6 +372,8 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) | ||||
| - [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) | ||||
| - [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers) | ||||
| - [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge) | ||||
|  | ||||
| ## Articles / Tutorials | ||||
|  | ||||
| @@ -318,6 +387,7 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) | ||||
| - <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/> | ||||
| - <https://userlinux.net/mattermost-and-matterbridge.html> | ||||
| - <https://nextcloud.com/blog/bridging-chat-services-in-talk/> | ||||
| - <https://minecraftchest1.wordpress.com/2021/06/05/how-to-install-and-setup-matterbridge/> | ||||
| - Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc) | ||||
|  | ||||
| ## Thanks | ||||
| @@ -333,10 +403,10 @@ Matterbridge wouldn't exist without these libraries: | ||||
|  | ||||
| - discord - <https://github.com/bwmarrin/discordgo> | ||||
| - echo - <https://github.com/labstack/echo> | ||||
| - gitter - <https://github.com/sromku/go-gitter> | ||||
| - gops - <https://github.com/google/gops> | ||||
| - gozulipbot - <https://github.com/ifo/gozulipbot> | ||||
| - gumble - <https://github.com/layeh/gumble> | ||||
| - harmony - <https://github.com/harmony-development/shibshib> | ||||
| - irc - <https://github.com/lrstanley/girc> | ||||
| - keybase - <https://github.com/keybase/go-keybase-chat-bot> | ||||
| - matrix - <https://github.com/matrix-org/gomatrix> | ||||
| @@ -344,6 +414,7 @@ Matterbridge wouldn't exist without these libraries: | ||||
| - msgraph.go - <https://github.com/yaegashi/msgraph.go> | ||||
| - mumble - <https://github.com/layeh/gumble> | ||||
| - nctalk - <https://github.com/gary-kim/go-nc-talk> | ||||
| - rocketchat - <https://github.com/RocketChat/Rocket.Chat.Go.SDK> | ||||
| - slack - <https://github.com/nlopes/slack> | ||||
| - sshchat - <https://github.com/shazow/ssh-chat> | ||||
| - steam - <https://github.com/Philipp15b/go-steam> | ||||
| @@ -351,6 +422,7 @@ Matterbridge wouldn't exist without these libraries: | ||||
| - tengo - <https://github.com/d5/tengo> | ||||
| - vk - <https://github.com/SevereCloud/vksdk> | ||||
| - whatsapp - <https://github.com/Rhymen/go-whatsapp> | ||||
| - whatsapp - <https://github.com/tulir/whatsmeow> | ||||
| - xmpp - <https://github.com/mattn/go-xmpp> | ||||
| - zulip - <https://github.com/ifo/gozulipbot> | ||||
|  | ||||
| @@ -358,7 +430,7 @@ Matterbridge wouldn't exist without these libraries: | ||||
|  | ||||
| [mb-discord]: https://discord.gg/AkKPtrQ | ||||
| [mb-gitter]: https://gitter.im/42wim/matterbridge | ||||
| [mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat | ||||
| [mb-irc]: https://web.libera.chat/#matterbridge | ||||
| [mb-keybase]: https://keybase.io/team/matterbridge | ||||
| [mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org | ||||
| [mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e | ||||
|   | ||||
| @@ -1,17 +1,20 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"gopkg.in/olahol/melody.v1" | ||||
| 	"github.com/olahol/melody" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/labstack/echo/v4" | ||||
| 	"github.com/labstack/echo/v4/middleware" | ||||
| 	"github.com/mitchellh/mapstructure" | ||||
| 	ring "github.com/zfjagann/golang-ring" | ||||
| ) | ||||
|  | ||||
| @@ -137,6 +140,36 @@ func (b *API) handlePostMessage(c echo.Context) error { | ||||
| 	message.Account = b.Account | ||||
| 	message.ID = "" | ||||
| 	message.Timestamp = time.Now() | ||||
|  | ||||
| 	var ( | ||||
| 		fm map[string]interface{} | ||||
| 		ds string | ||||
| 		ok bool | ||||
| 	) | ||||
|  | ||||
| 	for i, f := range message.Extra["file"] { | ||||
| 		fi := config.FileInfo{} | ||||
| 		if fm, ok = f.(map[string]interface{}); !ok { | ||||
| 			return echo.NewHTTPError(http.StatusInternalServerError, "invalid format for extra") | ||||
| 		} | ||||
| 		err := mapstructure.Decode(fm, &fi) | ||||
| 		if err != nil { | ||||
| 			if !strings.Contains(err.Error(), "got string") { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 		// mapstructure doesn't decode base64 into []byte, so it must be done manually for fi.Data | ||||
| 		if ds, ok = fm["Data"].(string); !ok { | ||||
| 			return echo.NewHTTPError(http.StatusInternalServerError, "invalid format for data") | ||||
| 		} | ||||
|  | ||||
| 		data, err := base64.StdEncoding.DecodeString(ds) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		fi.Data = &data | ||||
| 		message.Extra["file"][i] = fi | ||||
| 	} | ||||
| 	b.Log.Debugf("Sending message from %s on %s to gateway", message.Username, "api") | ||||
| 	b.Remote <- message | ||||
| 	return c.JSON(http.StatusOK, message) | ||||
| @@ -166,15 +199,20 @@ func (b *API) handleStream(c echo.Context) error { | ||||
| 	} | ||||
| 	c.Response().Flush() | ||||
| 	for { | ||||
| 		select { | ||||
| 		// TODO: this causes issues, messages should be broadcasted to all connected clients | ||||
| 		msg := b.Messages.Dequeue() | ||||
| 		if msg != nil { | ||||
| 			if err := json.NewEncoder(c.Response()).Encode(msg); err != nil { | ||||
| 				return err | ||||
| 		default: | ||||
| 			msg := b.Messages.Dequeue() | ||||
| 			if msg != nil { | ||||
| 				if err := json.NewEncoder(c.Response()).Encode(msg); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 				c.Response().Flush() | ||||
| 			} | ||||
| 			c.Response().Flush() | ||||
| 			time.Sleep(100 * time.Millisecond) | ||||
| 		case <-c.Request().Context().Done(): | ||||
| 			return nil | ||||
| 		} | ||||
| 		time.Sleep(200 * time.Millisecond) | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -23,6 +23,7 @@ const ( | ||||
| 	EventRejoinChannels    = "rejoin_channels" | ||||
| 	EventUserAction        = "user_action" | ||||
| 	EventMsgDelete         = "msg_delete" | ||||
| 	EventFileDelete        = "file_delete" | ||||
| 	EventAPIConnected      = "api_connected" | ||||
| 	EventUserTyping        = "user_typing" | ||||
| 	EventGetChannelMembers = "get_channel_members" | ||||
| @@ -56,13 +57,14 @@ func (m Message) ParentValid() bool { | ||||
| } | ||||
|  | ||||
| type FileInfo struct { | ||||
| 	Name    string | ||||
| 	Data    *[]byte | ||||
| 	Comment string | ||||
| 	URL     string | ||||
| 	Size    int64 | ||||
| 	Avatar  bool | ||||
| 	SHA     string | ||||
| 	Name     string | ||||
| 	Data     *[]byte | ||||
| 	Comment  string | ||||
| 	URL      string | ||||
| 	Size     int64 | ||||
| 	Avatar   bool | ||||
| 	SHA      string | ||||
| 	NativeID string | ||||
| } | ||||
|  | ||||
| type ChannelInfo struct { | ||||
| @@ -85,27 +87,28 @@ type ChannelMember struct { | ||||
| type ChannelMembers []ChannelMember | ||||
|  | ||||
| type Protocol struct { | ||||
| 	AuthCode               string // steam | ||||
| 	BindAddress            string // mattermost, slack // DEPRECATED | ||||
| 	Buffer                 int    // api | ||||
| 	Charset                string // irc | ||||
| 	ClientID               string // msteams | ||||
| 	ColorNicks             bool   // only irc for now | ||||
| 	Debug                  bool   // general | ||||
| 	DebugLevel             int    // only for irc now | ||||
| 	DisableWebPagePreview  bool   // telegram | ||||
| 	EditSuffix             string // mattermost, slack, discord, telegram, gitter | ||||
| 	EditDisable            bool   // mattermost, slack, discord, telegram, gitter | ||||
| 	HTMLDisable            bool   // matrix | ||||
| 	IconURL                string // mattermost, slack | ||||
| 	IgnoreFailureOnStart   bool   // general | ||||
| 	IgnoreNicks            string // all protocols | ||||
| 	IgnoreMessages         string // all protocols | ||||
| 	Jid                    string // xmpp | ||||
| 	JoinDelay              string // all protocols | ||||
| 	Label                  string // all protocols | ||||
| 	Login                  string // mattermost, matrix | ||||
| 	LogFile                string // general | ||||
| 	AllowMention           []string // discord | ||||
| 	AuthCode               string   // steam | ||||
| 	BindAddress            string   // mattermost, slack // DEPRECATED | ||||
| 	Buffer                 int      // api | ||||
| 	Charset                string   // irc | ||||
| 	ClientID               string   // msteams | ||||
| 	ColorNicks             bool     // only irc for now | ||||
| 	Debug                  bool     // general | ||||
| 	DebugLevel             int      // only for irc now | ||||
| 	DisableWebPagePreview  bool     // telegram | ||||
| 	EditSuffix             string   // mattermost, slack, discord, telegram, gitter | ||||
| 	EditDisable            bool     // mattermost, slack, discord, telegram, gitter | ||||
| 	HTMLDisable            bool     // matrix | ||||
| 	IconURL                string   // mattermost, slack | ||||
| 	IgnoreFailureOnStart   bool     // general | ||||
| 	IgnoreNicks            string   // all protocols | ||||
| 	IgnoreMessages         string   // all protocols | ||||
| 	Jid                    string   // xmpp | ||||
| 	JoinDelay              string   // all protocols | ||||
| 	Label                  string   // all protocols | ||||
| 	Login                  string   // mattermost, matrix | ||||
| 	LogFile                string   // general | ||||
| 	MediaDownloadBlackList []string | ||||
| 	MediaDownloadPath      string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server. | ||||
| 	MediaDownloadSize      int    // all protocols | ||||
| @@ -118,7 +121,9 @@ type Protocol struct { | ||||
| 	MessageLength          int        // IRC, max length of a message allowed | ||||
| 	MessageQueue           int        // IRC, size of message queue for flood control | ||||
| 	MessageSplit           bool       // IRC, split long messages with newlines on MessageLength instead of clipping | ||||
| 	MessageSplitMaxCount   int        // discord, split long messages into at most this many messages instead of clipping (MessageLength=1950 cannot be configured) | ||||
| 	Muc                    string     // xmpp | ||||
| 	MxID                   string     // matrix | ||||
| 	Name                   string     // all protocols | ||||
| 	Nick                   string     // all protocols | ||||
| 	NickFormatter          string     // mattermost, slack | ||||
| @@ -136,12 +141,13 @@ type Protocol struct { | ||||
| 	QuoteDisable           bool       // telegram | ||||
| 	QuoteFormat            string     // telegram | ||||
| 	QuoteLengthLimit       int        // telegram | ||||
| 	RealName               string     // IRC | ||||
| 	RejoinDelay            int        // IRC | ||||
| 	ReplaceMessages        [][]string // all protocols | ||||
| 	ReplaceNicks           [][]string // all protocols | ||||
| 	RemoteNickFormat       string     // all protocols | ||||
| 	RunCommands            []string   // IRC | ||||
| 	Server                 string     // IRC,mattermost,XMPP,discord | ||||
| 	Server                 string     // IRC,mattermost,XMPP,discord,matrix | ||||
| 	SessionFile            string     // msteams,whatsapp | ||||
| 	ShowJoinPart           bool       // all protocols | ||||
| 	ShowTopicChange        bool       // slack | ||||
| @@ -156,7 +162,7 @@ type Protocol struct { | ||||
| 	Team                   string     // mattermost, keybase | ||||
| 	TeamID                 string     // msteams | ||||
| 	TenantID               string     // msteams | ||||
| 	Token                  string     // gitter, slack, discord, api | ||||
| 	Token                  string     // gitter, slack, discord, api, matrix | ||||
| 	Topic                  string     // zulip | ||||
| 	URL                    string     // mattermost, slack // DEPRECATED | ||||
| 	UseAPI                 bool       // mattermost, slack | ||||
| @@ -165,8 +171,9 @@ type Protocol struct { | ||||
| 	UseTLS                 bool       // IRC | ||||
| 	UseDiscriminator       bool       // discord | ||||
| 	UseFirstName           bool       // telegram | ||||
| 	UseUserName            bool       // discord, matrix | ||||
| 	UseUserName            bool       // discord, matrix, mattermost | ||||
| 	UseInsecureURL         bool       // telegram | ||||
| 	UserName               string     // IRC | ||||
| 	VerboseJoinPart        bool       // IRC | ||||
| 	WebhookBindAddress     string     // mattermost, slack | ||||
| 	WebhookURL             string     // mattermost, slack | ||||
|   | ||||
| @@ -10,10 +10,14 @@ import ( | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/discord/transmitter" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/matterbridge/discordgo" | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| 	lru "github.com/hashicorp/golang-lru" | ||||
| ) | ||||
|  | ||||
| const MessageLength = 1950 | ||||
| const ( | ||||
| 	MessageLength = 1950 | ||||
| 	cFileUpload   = "file_upload" | ||||
| ) | ||||
|  | ||||
| type Bdiscord struct { | ||||
| 	*bridge.Config | ||||
| @@ -35,10 +39,20 @@ type Bdiscord struct { | ||||
| 	// Webhook specific logic | ||||
| 	useAutoWebhooks bool | ||||
| 	transmitter     *transmitter.Transmitter | ||||
| 	cache           *lru.Cache | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	b := &Bdiscord{Config: cfg} | ||||
| 	newCache, err := lru.New(5000) | ||||
| 	if err != nil { | ||||
| 		cfg.Log.Fatalf("Could not create LRU cache: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	b := &Bdiscord{ | ||||
| 		Config: cfg, | ||||
| 		cache:  newCache, | ||||
| 	} | ||||
|  | ||||
| 	b.userMemberMap = make(map[string]*discordgo.Member) | ||||
| 	b.nickMemberMap = make(map[string]*discordgo.Member) | ||||
| 	b.channelInfoMap = make(map[string]*config.ChannelInfo) | ||||
| @@ -67,19 +81,16 @@ func (b *Bdiscord) Connect() error { | ||||
| 		return err | ||||
| 	} | ||||
| 	b.Log.Info("Connection succeeded") | ||||
| 	b.c.AddHandler(b.messageCreate) | ||||
| 	b.c.AddHandler(b.messageTyping) | ||||
| 	b.c.AddHandler(b.memberUpdate) | ||||
| 	b.c.AddHandler(b.messageUpdate) | ||||
| 	b.c.AddHandler(b.messageDelete) | ||||
| 	b.c.AddHandler(b.messageDeleteBulk) | ||||
| 	b.c.AddHandler(b.memberAdd) | ||||
| 	b.c.AddHandler(b.memberRemove) | ||||
| 	// 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() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	guilds, err := b.c.UserGuilds(100, "", "") | ||||
| 	guilds, err := b.c.UserGuilds(100, "", "", false) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -148,7 +159,7 @@ func (b *Bdiscord) Connect() error { | ||||
| 		return fmt.Errorf("use of removed WebhookURL setting") | ||||
| 	} | ||||
|  | ||||
| 	if b.GetInt("debuglevel") > 0 { | ||||
| 	if b.GetInt("debuglevel") == 2 { | ||||
| 		b.Log.Debug("enabling even more discord debug") | ||||
| 		b.c.Debug = true | ||||
| 	} | ||||
| @@ -211,6 +222,19 @@ func (b *Bdiscord) Connect() error { | ||||
| 			b.nickMemberMap[member.Nick] = member | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	b.c.AddHandler(b.messageCreate) | ||||
| 	b.c.AddHandler(b.messageTyping) | ||||
| 	b.c.AddHandler(b.messageUpdate) | ||||
| 	b.c.AddHandler(b.messageDelete) | ||||
| 	b.c.AddHandler(b.messageDeleteBulk) | ||||
| 	b.c.AddHandler(b.memberAdd) | ||||
| 	b.c.AddHandler(b.memberRemove) | ||||
| 	b.c.AddHandler(b.memberUpdate) | ||||
| 	if b.GetInt("debuglevel") == 1 { | ||||
| 		b.c.AddHandler(b.messageEvent) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @@ -250,7 +274,6 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { | ||||
| 	// Handle prefix hint for unthreaded messages. | ||||
| 	if msg.ParentNotFound() { | ||||
| 		msg.ParentID = "" | ||||
| 		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) | ||||
| 	} | ||||
|  | ||||
| 	// Use webhook to send the message | ||||
| @@ -275,10 +298,26 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	// Delete a file | ||||
| 	if msg.Event == config.EventFileDelete { | ||||
| 		if msg.ID == "" { | ||||
| 			return "", nil | ||||
| 		} | ||||
|  | ||||
| 		if fi, ok := b.cache.Get(cFileUpload + msg.ID); ok { | ||||
| 			err := b.c.ChannelMessageDelete(channelID, fi.(string)) // nolint:forcetypeassert | ||||
| 			b.cache.Remove(cFileUpload + msg.ID) | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		return "", fmt.Errorf("file %s not found", msg.ID) | ||||
| 	} | ||||
|  | ||||
| 	// Upload a file if it exists | ||||
| 	if msg.Extra != nil { | ||||
| 		for _, rmsg := range helper.HandleExtra(msg, b.General) { | ||||
| 			rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength) | ||||
| 			// TODO: Use ClipOrSplitMessage | ||||
| 			rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped")) | ||||
| 			if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil { | ||||
| 				b.Log.Errorf("Could not send message %#v: %s", rmsg, err) | ||||
| 			} | ||||
| @@ -289,39 +328,57 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	msg.Text = helper.ClipMessage(msg.Text, MessageLength) | ||||
| 	msg.Text = b.replaceUserMentions(msg.Text) | ||||
|  | ||||
| 	// Edit message | ||||
| 	if msg.ID != "" { | ||||
| 		_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text) | ||||
| 		return msg.ID, err | ||||
| 	} | ||||
|  | ||||
| 	m := discordgo.MessageSend{ | ||||
| 		Content: msg.Username + msg.Text, | ||||
| 	} | ||||
|  | ||||
| 	if msg.ParentValid() { | ||||
| 		m.Reference = &discordgo.MessageReference{ | ||||
| 			MessageID: msg.ParentID, | ||||
| 			ChannelID: channelID, | ||||
| 			GuildID:   b.guildID, | ||||
| 		// Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";". | ||||
| 		msgIds := strings.Split(msg.ID, ";") | ||||
| 		msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds)) | ||||
| 		for len(msgParts) < len(msgIds) { | ||||
| 			msgParts = append(msgParts, "((obsoleted by edit))") | ||||
| 		} | ||||
| 		for i := range msgParts { | ||||
| 			// In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates. | ||||
| 			// TODO: Optimize away noop-updates of un-edited messages | ||||
| 			// TODO: Use RemoteNickFormat instead of this broken concatenation | ||||
| 			_, err := b.c.ChannelMessageEdit(channelID, msgIds[i], msg.Username+msgParts[i]) | ||||
| 			if err != nil { | ||||
| 				return "", err | ||||
| 			} | ||||
| 		} | ||||
| 		return msg.ID, nil | ||||
| 	} | ||||
|  | ||||
| 	// Post normal message | ||||
| 	res, err := b.c.ChannelMessageSendComplex(channelID, &m) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), b.GetInt("MessageSplitMaxCount")) | ||||
| 	msgIds := []string{} | ||||
|  | ||||
| 	for _, msgPart := range msgParts { | ||||
| 		m := discordgo.MessageSend{ | ||||
| 			Content:         msg.Username + msgPart, | ||||
| 			AllowedMentions: b.getAllowedMentions(), | ||||
| 		} | ||||
|  | ||||
| 		if msg.ParentValid() { | ||||
| 			m.Reference = &discordgo.MessageReference{ | ||||
| 				MessageID: msg.ParentID, | ||||
| 				ChannelID: channelID, | ||||
| 				GuildID:   b.guildID, | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Post normal message | ||||
| 		res, err := b.c.ChannelMessageSendComplex(channelID, &m) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		msgIds = append(msgIds, res.ID) | ||||
| 	} | ||||
|  | ||||
| 	return res.ID, nil | ||||
| 	// Exploit that a discord message ID is actually just a large number, so we encode a list of IDs by separating them with ";". | ||||
| 	return strings.Join(msgIds, ";"), nil | ||||
| } | ||||
|  | ||||
| // handleUploadFile handles native upload of files | ||||
| func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) { | ||||
| 	var err error | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi := f.(config.FileInfo) | ||||
| 		file := discordgo.File{ | ||||
| @@ -330,13 +387,19 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri | ||||
| 			Reader:      bytes.NewReader(*fi.Data), | ||||
| 		} | ||||
| 		m := discordgo.MessageSend{ | ||||
| 			Content: msg.Username + fi.Comment, | ||||
| 			Files:   []*discordgo.File{&file}, | ||||
| 			Content:         msg.Username + fi.Comment, | ||||
| 			Files:           []*discordgo.File{&file}, | ||||
| 			AllowedMentions: b.getAllowedMentions(), | ||||
| 		} | ||||
| 		_, err = b.c.ChannelMessageSendComplex(channelID, &m) | ||||
| 		res, err := b.c.ChannelMessageSendComplex(channelID, &m) | ||||
| 		if err != nil { | ||||
| 			return "", fmt.Errorf("file upload failed: %s", err) | ||||
| 		} | ||||
|  | ||||
| 		// link file_upload_nativeID (file ID from the original bridge) to our upload id | ||||
| 		// so that we can remove this later when it eg needs to be deleted | ||||
| 		b.cache.Add(cFileUpload+fi.NativeID, res.ID) | ||||
| 	} | ||||
|  | ||||
| 	return "", nil | ||||
| } | ||||
|   | ||||
| @@ -2,10 +2,15 @@ package bdiscord | ||||
|  | ||||
| import ( | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/matterbridge/discordgo" | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| 	"github.com/davecgh/go-spew/spew" | ||||
| ) | ||||
|  | ||||
| func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam | ||||
| 	if m.GuildID != b.guildID { | ||||
| 		b.Log.Debugf("Ignoring messageDelete because it originates from a different guild") | ||||
| 		return | ||||
| 	} | ||||
| 	rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete} | ||||
| 	rmsg.Channel = b.getChannelName(m.ChannelID) | ||||
|  | ||||
| @@ -16,6 +21,10 @@ func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelet | ||||
|  | ||||
| // 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 | ||||
| 	if m.GuildID != b.guildID { | ||||
| 		b.Log.Debugf("Ignoring messageDeleteBulk because it originates from a different guild") | ||||
| 		return | ||||
| 	} | ||||
| 	for _, msgID := range m.Messages { | ||||
| 		rmsg := config.Message{ | ||||
| 			Account: b.Account, | ||||
| @@ -31,7 +40,15 @@ func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageD | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) messageEvent(s *discordgo.Session, m *discordgo.Event) { | ||||
| 	b.Log.Debug(spew.Sdump(m.Struct)) | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) { | ||||
| 	if m.GuildID != b.guildID { | ||||
| 		b.Log.Debugf("Ignoring messageTyping because it originates from a different guild") | ||||
| 		return | ||||
| 	} | ||||
| 	if !b.GetBool("ShowUserTyping") { | ||||
| 		return | ||||
| 	} | ||||
| @@ -47,11 +64,15 @@ func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam | ||||
| 	if m.GuildID != b.guildID { | ||||
| 		b.Log.Debugf("Ignoring messageUpdate because it originates from a different guild") | ||||
| 		return | ||||
| 	} | ||||
| 	if b.GetBool("EditDisable") { | ||||
| 		return | ||||
| 	} | ||||
| 	// only when message is actually edited | ||||
| 	if m.Message.EditedTimestamp != "" { | ||||
| 	if m.Message.EditedTimestamp != nil { | ||||
| 		b.Log.Debugf("Sending edit message") | ||||
| 		m.Content += b.GetString("EditSuffix") | ||||
| 		msg := &discordgo.MessageCreate{ | ||||
| @@ -62,6 +83,10 @@ func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdat | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam | ||||
| 	if m.GuildID != b.guildID { | ||||
| 		b.Log.Debugf("Ignoring messageCreate because it originates from a different guild") | ||||
| 		return | ||||
| 	} | ||||
| 	var err error | ||||
|  | ||||
| 	// not relay our own messages | ||||
| @@ -82,8 +107,9 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat | ||||
|  | ||||
| 	rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID} | ||||
|  | ||||
| 	b.Log.Debugf("== Receiving event %#v", m.Message) | ||||
|  | ||||
| 	if m.Content != "" { | ||||
| 		b.Log.Debugf("== Receiving event %#v", m.Message) | ||||
| 		m.Message.Content = b.replaceChannelMentions(m.Message.Content) | ||||
| 		rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c) | ||||
| 		if err != nil { | ||||
| @@ -138,6 +164,10 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { | ||||
| 	if m.GuildID != b.guildID { | ||||
| 		b.Log.Debugf("Ignoring memberUpdate because it originates from a different guild") | ||||
| 		return | ||||
| 	} | ||||
| 	if m.Member == nil { | ||||
| 		b.Log.Warnf("Received member update with no member information: %#v", m) | ||||
| 	} | ||||
| @@ -165,6 +195,13 @@ func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUp | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) { | ||||
| 	if m.GuildID != b.guildID { | ||||
| 		b.Log.Debugf("Ignoring memberAdd because it originates from a different guild") | ||||
| 		return | ||||
| 	} | ||||
| 	if b.GetBool("nosendjoinpart") { | ||||
| 		return | ||||
| 	} | ||||
| 	if m.Member == nil { | ||||
| 		b.Log.Warnf("Received member update with no member information: %#v", m) | ||||
| 		return | ||||
| @@ -186,6 +223,13 @@ func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) { | ||||
| 	if m.GuildID != b.guildID { | ||||
| 		b.Log.Debugf("Ignoring memberRemove because it originates from a different guild") | ||||
| 		return | ||||
| 	} | ||||
| 	if b.GetBool("nosendjoinpart") { | ||||
| 		return | ||||
| 	} | ||||
| 	if m.Member == nil { | ||||
| 		b.Log.Warnf("Received member update with no member information: %#v", m) | ||||
| 		return | ||||
|   | ||||
| @@ -3,7 +3,7 @@ package bdiscord | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/matterbridge/discordgo" | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
|   | ||||
| @@ -6,9 +6,33 @@ import ( | ||||
| 	"strings" | ||||
| 	"unicode" | ||||
|  | ||||
| 	"github.com/matterbridge/discordgo" | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| ) | ||||
|  | ||||
| func (b *Bdiscord) getAllowedMentions() *discordgo.MessageAllowedMentions { | ||||
| 	// If AllowMention is not specified, then allow all mentions (default Discord behavior) | ||||
| 	if !b.IsKeySet("AllowMention") { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// Otherwise, allow only the mentions that are specified | ||||
| 	allowedMentionTypes := make([]discordgo.AllowedMentionType, 0, 3) | ||||
| 	for _, m := range b.GetStringSlice("AllowMention") { | ||||
| 		switch m { | ||||
| 		case "everyone": | ||||
| 			allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeEveryone) | ||||
| 		case "roles": | ||||
| 			allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeRoles) | ||||
| 		case "users": | ||||
| 			allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeUsers) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return &discordgo.MessageAllowedMentions{ | ||||
| 		Parse: allowedMentionTypes, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string { | ||||
| 	b.membersMutex.RLock() | ||||
| 	defer b.membersMutex.RUnlock() | ||||
| @@ -44,7 +68,7 @@ func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error) | ||||
| 	b.membersMutex.RLock() | ||||
| 	defer b.membersMutex.RUnlock() | ||||
|  | ||||
| 	if member, ok := b.nickMemberMap[nick]; ok { | ||||
| 	if member, ok := b.nickMemberMap[strings.TrimSpace(nick)]; ok { | ||||
| 		return member, nil | ||||
| 	} | ||||
| 	return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller | ||||
|   | ||||
| @@ -20,7 +20,7 @@ import ( | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/matterbridge/discordgo" | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| @@ -58,7 +58,7 @@ func New(session *discordgo.Session, guild string, title string, autoCreate bool | ||||
|  | ||||
| 		channelWebhooks: make(map[string]*discordgo.Webhook), | ||||
|  | ||||
| 		Log: log.NewEntry(nil), | ||||
| 		Log: log.NewEntry(log.StandardLogger()), | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package transmitter | ||||
|  | ||||
| import ( | ||||
| 	"github.com/matterbridge/discordgo" | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| ) | ||||
|  | ||||
| // isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions | ||||
|   | ||||
| @@ -2,10 +2,11 @@ package bdiscord | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/matterbridge/discordgo" | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| ) | ||||
|  | ||||
| // shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks | ||||
| @@ -42,12 +43,65 @@ func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string { | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) webhookSendTextOnly(msg *config.Message, channelID string) (string, error) { | ||||
| 	msgParts := helper.ClipOrSplitMessage(msg.Text, MessageLength, b.GetString("MessageClipped"), b.GetInt("MessageSplitMaxCount")) | ||||
| 	msgIds := []string{} | ||||
| 	for _, msgPart := range msgParts { | ||||
| 		res, err := b.transmitter.Send( | ||||
| 			channelID, | ||||
| 			&discordgo.WebhookParams{ | ||||
| 				Content:         msgPart, | ||||
| 				Username:        msg.Username, | ||||
| 				AvatarURL:       msg.Avatar, | ||||
| 				AllowedMentions: b.getAllowedMentions(), | ||||
| 			}, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} else { | ||||
| 			msgIds = append(msgIds, res.ID) | ||||
| 		} | ||||
| 	} | ||||
| 	// Exploit that a discord message ID is actually just a large number, so we encode a list of IDs by separating them with ";". | ||||
| 	return strings.Join(msgIds, ";"), nil | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) webhookSendFilesOnly(msg *config.Message, channelID string) error { | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi := f.(config.FileInfo) //nolint:forcetypeassert | ||||
| 		file := discordgo.File{ | ||||
| 			Name:        fi.Name, | ||||
| 			ContentType: "", | ||||
| 			Reader:      bytes.NewReader(*fi.Data), | ||||
| 		} | ||||
| 		content := fi.Comment | ||||
|  | ||||
| 		// Cannot use the resulting ID for any edits anyway, so throw it away. | ||||
| 		// This has to be re-enabled when we implement message deletion. | ||||
| 		_, err := b.transmitter.Send( | ||||
| 			channelID, | ||||
| 			&discordgo.WebhookParams{ | ||||
| 				Username:        msg.Username, | ||||
| 				AvatarURL:       msg.Avatar, | ||||
| 				Files:           []*discordgo.File{&file}, | ||||
| 				Content:         content, | ||||
| 				AllowedMentions: b.getAllowedMentions(), | ||||
| 			}, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, err) | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // webhookSend send one or more message via webhook, taking care of file | ||||
| // uploads (from slack, telegram or mattermost). | ||||
| // Returns messageID and error. | ||||
| func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) { | ||||
| func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (string, error) { | ||||
| 	var ( | ||||
| 		res *discordgo.Message | ||||
| 		res string | ||||
| 		err error | ||||
| 	) | ||||
|  | ||||
| @@ -60,45 +114,13 @@ func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordg | ||||
|  | ||||
| 	// 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) | ||||
| 		} | ||||
| 		res, err = b.webhookSendTextOnly(msg, channelID) | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
| 			} | ||||
| 		} | ||||
| 	if err == nil && msg.Extra != nil { | ||||
| 		err = b.webhookSendFilesOnly(msg, channelID) | ||||
| 	} | ||||
|  | ||||
| 	return res, err | ||||
| } | ||||
|  | ||||
| @@ -114,34 +136,44 @@ func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (st | ||||
| 		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 != "" { | ||||
| 		// Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";". | ||||
| 		msgIds := strings.Split(msg.ID, ";") | ||||
| 		msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds)) | ||||
| 		for len(msgParts) < len(msgIds) { | ||||
| 			msgParts = append(msgParts, "((obsoleted by edit))") | ||||
| 		} | ||||
| 		b.Log.Debugf("Editing webhook message") | ||||
| 		err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{ | ||||
| 			Content:  msg.Text, | ||||
| 			Username: msg.Username, | ||||
| 		}) | ||||
| 		if err == nil { | ||||
| 		var editErr error = nil | ||||
| 		for i := range msgParts { | ||||
| 			// In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates. | ||||
| 			// TODO: Optimize away noop-updates of un-edited messages | ||||
| 			editErr = b.transmitter.Edit(channelID, msgIds[i], &discordgo.WebhookParams{ | ||||
| 				Content:         msgParts[i], | ||||
| 				Username:        msg.Username, | ||||
| 				AllowedMentions: b.getAllowedMentions(), | ||||
| 			}) | ||||
| 			if editErr != nil { | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if editErr == nil { | ||||
| 			return msg.ID, nil | ||||
| 		} | ||||
| 		b.Log.Errorf("Could not edit webhook message: %s", err) | ||||
| 		b.Log.Errorf("Could not edit webhook message(s): %s; sending as new message(s) instead", editErr) | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("Processing webhook sending for message %#v", msg) | ||||
| 	discordMsg, err := b.webhookSend(msg, channelID) | ||||
| 	msg.Text = b.replaceUserMentions(msg.Text) | ||||
| 	msgID, err := b.webhookSend(msg, channelID) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msg, err) | ||||
| 		b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msgID, err) | ||||
| 		return "", err | ||||
| 	} | ||||
| 	if discordMsg == nil { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	return discordMsg.ID, nil | ||||
| 	return msgID, nil | ||||
| } | ||||
|   | ||||
| @@ -1,182 +0,0 @@ | ||||
| package bgitter | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/42wim/go-gitter" | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| ) | ||||
|  | ||||
| type Bgitter struct { | ||||
| 	c     *gitter.Gitter | ||||
| 	User  *gitter.User | ||||
| 	Users []gitter.User | ||||
| 	Rooms []gitter.Room | ||||
| 	*bridge.Config | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	return &Bgitter{Config: cfg} | ||||
| } | ||||
|  | ||||
| func (b *Bgitter) Connect() error { | ||||
| 	var err error | ||||
| 	b.Log.Info("Connecting") | ||||
| 	b.c = gitter.New(b.GetString("Token")) | ||||
| 	b.User, err = b.c.GetUser() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	b.Rooms, err = b.c.GetRooms() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	b.Log.Info("Connection succeeded") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bgitter) Disconnect() error { | ||||
| 	return nil | ||||
|  | ||||
| } | ||||
|  | ||||
| func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	roomID, err := b.c.GetRoomId(channel.Name) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Could not find roomID for %v. Please create the room on gitter.im", channel.Name) | ||||
| 	} | ||||
| 	room, err := b.c.GetRoom(roomID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	b.Rooms = append(b.Rooms, *room) | ||||
| 	user, err := b.c.GetUser() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	_, err = b.c.JoinRoom(roomID, user.ID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	users, _ := b.c.GetUsersInRoom(roomID) | ||||
| 	b.Users = append(b.Users, users...) | ||||
| 	stream := b.c.Stream(roomID) | ||||
| 	go b.c.Listen(stream) | ||||
|  | ||||
| 	go func(stream *gitter.Stream, room string) { | ||||
| 		for event := range stream.Event { | ||||
| 			switch ev := event.Data.(type) { | ||||
| 			case *gitter.MessageReceived: | ||||
| 				// ignore message sent from ourselves | ||||
| 				if ev.Message.From.ID != b.User.ID { | ||||
| 					b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account) | ||||
| 					rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room, | ||||
| 						Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID, | ||||
| 						ID: ev.Message.ID} | ||||
| 					if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) { | ||||
| 						rmsg.Event = config.EventUserAction | ||||
| 						rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1) | ||||
| 					} | ||||
| 					b.Log.Debugf("<= Message is %#v", rmsg) | ||||
| 					b.Remote <- rmsg | ||||
| 				} | ||||
| 			case *gitter.GitterConnectionClosed: | ||||
| 				b.Log.Errorf("connection with gitter closed for room %s", room) | ||||
| 			} | ||||
| 		} | ||||
| 	}(stream, room.URI) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bgitter) Send(msg config.Message) (string, error) { | ||||
| 	b.Log.Debugf("=> Receiving %#v", msg) | ||||
| 	roomID := b.getRoomID(msg.Channel) | ||||
| 	if roomID == "" { | ||||
| 		b.Log.Errorf("Could not find roomID for %v", msg.Channel) | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	// Delete message | ||||
| 	if msg.Event == config.EventMsgDelete { | ||||
| 		if msg.ID == "" { | ||||
| 			return "", nil | ||||
| 		} | ||||
| 		// gitter has no delete message api so we edit message to "" | ||||
| 		_, err := b.c.UpdateMessage(roomID, msg.ID, "") | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	// Upload a file (in gitter case send the upload URL because gitter has no native upload support) | ||||
| 	if msg.Extra != nil { | ||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | ||||
| 			b.c.SendMessage(roomID, rmsg.Username+rmsg.Text) | ||||
| 		} | ||||
| 		if len(msg.Extra["file"]) > 0 { | ||||
| 			return b.handleUploadFile(&msg, roomID) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Edit message | ||||
| 	if msg.ID != "" { | ||||
| 		b.Log.Debugf("updating message with id %s", msg.ID) | ||||
| 		_, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	// Post normal message | ||||
| 	resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return resp.ID, nil | ||||
| } | ||||
|  | ||||
| func (b *Bgitter) getRoomID(channel string) string { | ||||
| 	for _, v := range b.Rooms { | ||||
| 		if v.URI == channel { | ||||
| 			return v.ID | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (b *Bgitter) getAvatar(user string) string { | ||||
| 	var avatar string | ||||
| 	if b.Users != nil { | ||||
| 		for _, u := range b.Users { | ||||
| 			if user == u.Username { | ||||
| 				return u.AvatarURLSmall | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return avatar | ||||
| } | ||||
|  | ||||
| func (b *Bgitter) handleUploadFile(msg *config.Message, roomID string) (string, error) { | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi := f.(config.FileInfo) | ||||
| 		if fi.Comment != "" { | ||||
| 			msg.Text += fi.Comment + ": " | ||||
| 		} | ||||
| 		if fi.URL != "" { | ||||
| 			msg.Text = fi.URL | ||||
| 			if fi.Comment != "" { | ||||
| 				msg.Text = fi.Comment + ": " + fi.URL | ||||
| 			} | ||||
| 		} | ||||
| 		_, err := b.c.SendMessage(roomID, msg.Username+msg.Text) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 	} | ||||
| 	return "", nil | ||||
| } | ||||
							
								
								
									
										252
									
								
								bridge/harmony/harmony.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,252 @@ | ||||
| package harmony | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/harmony-development/shibshib" | ||||
| 	chatv1 "github.com/harmony-development/shibshib/gen/chat/v1" | ||||
| 	typesv1 "github.com/harmony-development/shibshib/gen/harmonytypes/v1" | ||||
| 	profilev1 "github.com/harmony-development/shibshib/gen/profile/v1" | ||||
| ) | ||||
|  | ||||
| type cachedProfile struct { | ||||
| 	data        *profilev1.GetProfileResponse | ||||
| 	lastUpdated time.Time | ||||
| } | ||||
|  | ||||
| type Bharmony struct { | ||||
| 	*bridge.Config | ||||
|  | ||||
| 	c            *shibshib.Client | ||||
| 	profileCache map[uint64]cachedProfile | ||||
| } | ||||
|  | ||||
| func uToStr(in uint64) string { | ||||
| 	return strconv.FormatUint(in, 10) | ||||
| } | ||||
|  | ||||
| func strToU(in string) (uint64, error) { | ||||
| 	return strconv.ParseUint(in, 10, 64) | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	b := &Bharmony{ | ||||
| 		Config:       cfg, | ||||
| 		profileCache: map[uint64]cachedProfile{}, | ||||
| 	} | ||||
|  | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) getProfile(u uint64) (*profilev1.GetProfileResponse, error) { | ||||
| 	if v, ok := b.profileCache[u]; ok && time.Since(v.lastUpdated) < time.Minute*10 { | ||||
| 		return v.data, nil | ||||
| 	} | ||||
|  | ||||
| 	resp, err := b.c.ProfileKit.GetProfile(&profilev1.GetProfileRequest{ | ||||
| 		UserId: u, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		if v, ok := b.profileCache[u]; ok { | ||||
| 			return v.data, nil | ||||
| 		} | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	b.profileCache[u] = cachedProfile{ | ||||
| 		data:        resp, | ||||
| 		lastUpdated: time.Now(), | ||||
| 	} | ||||
| 	return resp, nil | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) avatarFor(m *chatv1.Message) string { | ||||
| 	if m.Overrides != nil { | ||||
| 		return m.Overrides.GetAvatar() | ||||
| 	} | ||||
|  | ||||
| 	profi, err := b.getProfile(m.AuthorId) | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	return b.c.TransformHMCURL(profi.Profile.GetUserAvatar()) | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) usernameFor(m *chatv1.Message) string { | ||||
| 	if m.Overrides != nil { | ||||
| 		return m.Overrides.GetUsername() | ||||
| 	} | ||||
|  | ||||
| 	profi, err := b.getProfile(m.AuthorId) | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	return profi.Profile.UserName | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) toMessage(msg *shibshib.LocatedMessage) config.Message { | ||||
| 	message := config.Message{} | ||||
| 	message.Account = b.Account | ||||
| 	message.UserID = uToStr(msg.Message.AuthorId) | ||||
| 	message.Avatar = b.avatarFor(msg.Message) | ||||
| 	message.Username = b.usernameFor(msg.Message) | ||||
| 	message.Channel = uToStr(msg.ChannelID) | ||||
| 	message.ID = uToStr(msg.MessageId) | ||||
|  | ||||
| 	switch content := msg.Message.Content.Content.(type) { | ||||
| 	case *chatv1.Content_EmbedMessage: | ||||
| 		message.Text = "Embed" | ||||
| 	case *chatv1.Content_AttachmentMessage: | ||||
| 		var s strings.Builder | ||||
| 		for idx, attach := range content.AttachmentMessage.Files { | ||||
| 			s.WriteString(b.c.TransformHMCURL(attach.Id)) | ||||
| 			if idx < len(content.AttachmentMessage.Files)-1 { | ||||
| 				s.WriteString(", ") | ||||
| 			} | ||||
| 		} | ||||
| 		message.Text = s.String() | ||||
| 	case *chatv1.Content_PhotoMessage: | ||||
| 		var s strings.Builder | ||||
| 		for idx, attach := range content.PhotoMessage.GetPhotos() { | ||||
| 			s.WriteString(attach.GetCaption().GetText()) | ||||
| 			s.WriteString("\n") | ||||
| 			s.WriteString(b.c.TransformHMCURL(attach.GetHmc())) | ||||
| 			if idx < len(content.PhotoMessage.GetPhotos())-1 { | ||||
| 				s.WriteString("\n\n") | ||||
| 			} | ||||
| 		} | ||||
| 		message.Text = s.String() | ||||
| 	case *chatv1.Content_TextMessage: | ||||
| 		message.Text = content.TextMessage.Content.Text | ||||
| 	} | ||||
|  | ||||
| 	return message | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) outputMessages() { | ||||
| 	for { | ||||
| 		msg := <-b.c.EventsStream() | ||||
|  | ||||
| 		if msg.Message.AuthorId == b.c.UserID { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		b.Remote <- b.toMessage(msg) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) GetUint64(conf string) uint64 { | ||||
| 	num, err := strToU(b.GetString(conf)) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	return num | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) Connect() (err error) { | ||||
| 	b.c, err = shibshib.NewClient(b.GetString("Homeserver"), b.GetString("Token"), b.GetUint64("UserID")) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	b.c.SubscribeToGuild(b.GetUint64("Community")) | ||||
|  | ||||
| 	go b.outputMessages() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) send(msg config.Message) (id string, err error) { | ||||
| 	msgChan, err := strToU(msg.Channel) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	retID, err := b.c.ChatKit.SendMessage(&chatv1.SendMessageRequest{ | ||||
| 		GuildId:   b.GetUint64("Community"), | ||||
| 		ChannelId: msgChan, | ||||
| 		Content: &chatv1.Content{ | ||||
| 			Content: &chatv1.Content_TextMessage{ | ||||
| 				TextMessage: &chatv1.Content_TextContent{ | ||||
| 					Content: &chatv1.FormattedText{ | ||||
| 						Text: msg.Text, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		Overrides: &chatv1.Overrides{ | ||||
| 			Username: &msg.Username, | ||||
| 			Avatar:   &msg.Avatar, | ||||
| 			Reason:   &chatv1.Overrides_Bridge{Bridge: &typesv1.Empty{}}, | ||||
| 		}, | ||||
| 		InReplyTo: nil, | ||||
| 		EchoId:    nil, | ||||
| 		Metadata:  nil, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		err = fmt.Errorf("send: error sending message: %w", err) | ||||
| 		log.Println(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	return uToStr(retID.MessageId), err | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) delete(msg config.Message) (id string, err error) { | ||||
| 	msgChan, err := strToU(msg.Channel) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	msgID, err := strToU(msg.ID) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	_, err = b.c.ChatKit.DeleteMessage(&chatv1.DeleteMessageRequest{ | ||||
| 		GuildId:   b.GetUint64("Community"), | ||||
| 		ChannelId: msgChan, | ||||
| 		MessageId: msgID, | ||||
| 	}) | ||||
| 	return "", err | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) typing(msg config.Message) (id string, err error) { | ||||
| 	msgChan, err := strToU(msg.Channel) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	_, err = b.c.ChatKit.Typing(&chatv1.TypingRequest{ | ||||
| 		GuildId:   b.GetUint64("Community"), | ||||
| 		ChannelId: msgChan, | ||||
| 	}) | ||||
| 	return "", err | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) Send(msg config.Message) (id string, err error) { | ||||
| 	switch msg.Event { | ||||
| 	case "": | ||||
| 		return b.send(msg) | ||||
| 	case config.EventMsgDelete: | ||||
| 		return b.delete(msg) | ||||
| 	case config.EventUserTyping: | ||||
| 		return b.typing(msg) | ||||
| 	default: | ||||
| 		return "", nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bharmony) Disconnect() error { | ||||
| 	return nil | ||||
| } | ||||
| @@ -5,10 +5,7 @@ import ( | ||||
| 	"fmt" | ||||
| 	"image/png" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| @@ -51,6 +48,30 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) { | ||||
| 	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 | ||||
| // specified as non-zero GetSubLines will also clip long lines to the maximum | ||||
| // length and insert a warning marker that the line was clipped. | ||||
| @@ -58,11 +79,19 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) { | ||||
| // TODO: The current implementation has the inconvenient that it disregards | ||||
| // word boundaries when splitting but this is hard to solve without potentially | ||||
| // breaking formatting and other stylistic effects. | ||||
| func GetSubLines(message string, maxLineLength int) []string { | ||||
| 	const clippingMessage = " <clipped message>" | ||||
| func GetSubLines(message string, maxLineLength int, clippingMessage string) []string { | ||||
| 	if clippingMessage == "" { | ||||
| 		clippingMessage = " <clipped message>" | ||||
| 	} | ||||
|  | ||||
| 	var lines []string | ||||
| 	for _, line := range strings.Split(strings.TrimSpace(message), "\n") { | ||||
| 		if line == "" { | ||||
| 			// Prevent sending empty messages, so we'll skip this line | ||||
| 			// if it has no content. | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if maxLineLength == 0 || len([]byte(line)) <= maxLineLength { | ||||
| 			lines = append(lines, line) | ||||
| 			continue | ||||
| @@ -145,17 +174,23 @@ func HandleDownloadSize(logger *logrus.Entry, msg *config.Message, name string, | ||||
|  | ||||
| // HandleDownloadData adds the data for a remote file into a Matterbridge gateway message. | ||||
| func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) { | ||||
| 	HandleDownloadData2(logger, msg, name, "", comment, url, data, general) | ||||
| } | ||||
|  | ||||
| // HandleDownloadData adds the data for a remote file into a Matterbridge gateway message. | ||||
| func HandleDownloadData2(logger *logrus.Entry, msg *config.Message, name, id, comment, url string, data *[]byte, general *config.Protocol) { | ||||
| 	var avatar bool | ||||
| 	logger.Debugf("Download OK %#v %#v", name, len(*data)) | ||||
| 	if msg.Event == config.EventAvatarDownload { | ||||
| 		avatar = true | ||||
| 	} | ||||
| 	msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{ | ||||
| 		Name:    name, | ||||
| 		Data:    data, | ||||
| 		URL:     url, | ||||
| 		Comment: comment, | ||||
| 		Avatar:  avatar, | ||||
| 		Name:     name, | ||||
| 		Data:     data, | ||||
| 		URL:      url, | ||||
| 		Comment:  comment, | ||||
| 		Avatar:   avatar, | ||||
| 		NativeID: id, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| @@ -169,18 +204,58 @@ func RemoveEmptyNewLines(msg string) string { | ||||
|  | ||||
| // ClipMessage trims a message to the specified length if it exceeds it and adds a warning | ||||
| // to the message in case it does so. | ||||
| func ClipMessage(text string, length int) string { | ||||
| 	const clippingMessage = " <clipped message>" | ||||
| func ClipMessage(text string, length int, clippingMessage string) string { | ||||
| 	if clippingMessage == "" { | ||||
| 		clippingMessage = " <clipped message>" | ||||
| 	} | ||||
|  | ||||
| 	if len(text) > length { | ||||
| 		text = text[:length-len(clippingMessage)] | ||||
| 		if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError { | ||||
| 			text = text[:len(text)-size] | ||||
| 		for len(text) > 0 { | ||||
| 			if r, _ := utf8.DecodeLastRuneInString(text); r == utf8.RuneError { | ||||
| 				text = text[:len(text)-1] | ||||
| 				// Note: DecodeLastRuneInString only returns the constant value "1" in | ||||
| 				// case of an error. We do not yet know whether the last rune is now | ||||
| 				// actually valid. Example: "€" is 0xE2 0x82 0xAC. If we happen to split | ||||
| 				// the string just before 0xAC, and go back only one byte, that would | ||||
| 				// leave us with a string that ends in the byte 0xE2, which is not a valid | ||||
| 				// rune, so we need to try again. | ||||
| 			} else { | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		text += clippingMessage | ||||
| 	} | ||||
| 	return text | ||||
| } | ||||
|  | ||||
| func ClipOrSplitMessage(text string, length int, clippingMessage string, splitMax int) []string { | ||||
| 	var msgParts []string | ||||
| 	remainingText := text | ||||
| 	// Invariant of this splitting loop: No text is lost (msgParts+remainingText is the original text), | ||||
| 	// and all parts is guaranteed to satisfy the length requirement. | ||||
| 	for len(msgParts) < splitMax-1 && len(remainingText) > length { | ||||
| 		// Decision: The text needs to be split (again). | ||||
| 		var chunk string | ||||
| 		wasted := 0 | ||||
| 		// The longest UTF-8 encoding of a valid rune is 4 bytes (0xF4 0x8F 0xBF 0xBF, encoding U+10FFFF), | ||||
| 		// so we should never need to waste 4 or more bytes at a time. | ||||
| 		for wasted < 4 && wasted < length { | ||||
| 			chunk = remainingText[:length-wasted] | ||||
| 			if r, _ := utf8.DecodeLastRuneInString(chunk); r == utf8.RuneError { | ||||
| 				wasted += 1 | ||||
| 			} else { | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		// Note: At this point, "chunk" might still be invalid, if "text" is very broken. | ||||
| 		msgParts = append(msgParts, chunk) | ||||
| 		remainingText = remainingText[len(chunk):] | ||||
| 	} | ||||
| 	msgParts = append(msgParts, ClipMessage(remainingText, length, clippingMessage)) | ||||
| 	return msgParts | ||||
| } | ||||
|  | ||||
| // ParseMarkdown takes in an input string as markdown and parses it to html | ||||
| func ParseMarkdown(input string) string { | ||||
| 	extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode | ||||
| @@ -210,49 +285,3 @@ func ConvertWebPToPNG(data *[]byte) error { | ||||
| 	*data = w.Bytes() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // CanConvertTgsToX Checks whether the external command necessary for ConvertTgsToX works. | ||||
| func CanConvertTgsToX() error { | ||||
| 	// We depend on the fact that `lottie_convert.py --help` has exit status 0. | ||||
| 	// Hyrum's Law predicted this, and Murphy's Law predicts that this will break eventually. | ||||
| 	// However, there is no alternative like `lottie_convert.py --is-properly-installed` | ||||
| 	cmd := exec.Command("lottie_convert.py", "--help") | ||||
| 	return cmd.Run() | ||||
| } | ||||
|  | ||||
| // ConvertTgsToWebP convert input data (which should be tgs format) to WebP format | ||||
| // This relies on an external command, which is ugly, but works. | ||||
| func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error { | ||||
| 	// lottie can't handle input from a pipe, so write to a temporary file: | ||||
| 	tmpFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-*.tgs") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	tmpFileName := tmpFile.Name() | ||||
| 	defer func() { | ||||
| 		if removeErr := os.Remove(tmpFileName); removeErr != nil { | ||||
| 			logger.Errorf("Could not delete temporary file %s: %v", tmpFileName, removeErr) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	if _, writeErr := tmpFile.Write(*data); writeErr != nil { | ||||
| 		return writeErr | ||||
| 	} | ||||
| 	// Must close before calling lottie to avoid data races: | ||||
| 	if closeErr := tmpFile.Close(); closeErr != nil { | ||||
| 		return closeErr | ||||
| 	} | ||||
|  | ||||
| 	// Call lottie to transform: | ||||
| 	cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpFileName, "/dev/stdout") | ||||
| 	cmd.Stderr = nil | ||||
| 	// NB: lottie writes progress into to stderr in all cases. | ||||
| 	stdout, stderr := cmd.Output() | ||||
| 	if stderr != nil { | ||||
| 		// 'stderr' already contains some parts of Stderr, because it was set to 'nil'. | ||||
| 		return stderr | ||||
| 	} | ||||
|  | ||||
| 	*data = stdout | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -10,98 +10,105 @@ import ( | ||||
|  | ||||
| const testLineLength = 64 | ||||
|  | ||||
| var ( | ||||
| 	lineSplittingTestCases = map[string]struct { | ||||
| 		input          string | ||||
| 		splitOutput    []string | ||||
| 		nonSplitOutput []string | ||||
| 	}{ | ||||
| 		"Short single-line message": { | ||||
| 			input:          "short", | ||||
| 			splitOutput:    []string{"short"}, | ||||
| 			nonSplitOutput: []string{"short"}, | ||||
| var lineSplittingTestCases = map[string]struct { | ||||
| 	input          string | ||||
| 	splitOutput    []string | ||||
| 	nonSplitOutput []string | ||||
| }{ | ||||
| 	"Short single-line message": { | ||||
| 		input:          "short", | ||||
| 		splitOutput:    []string{"short"}, | ||||
| 		nonSplitOutput: []string{"short"}, | ||||
| 	}, | ||||
| 	"Long single-line message": { | ||||
| 		input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", | ||||
| 		splitOutput: []string{ | ||||
| 			"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>", | ||||
| 			"cing elit, sed do eiusmod tempor incididunt ut <clipped message>", | ||||
| 			" labore et dolore magna aliqua.", | ||||
| 		}, | ||||
| 		"Long single-line message": { | ||||
| 			input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", | ||||
| 			splitOutput: []string{ | ||||
| 				"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>", | ||||
| 				"cing elit, sed do eiusmod tempor incididunt ut <clipped message>", | ||||
| 				" labore et dolore magna aliqua.", | ||||
| 			}, | ||||
| 			nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}, | ||||
| 		nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}, | ||||
| 	}, | ||||
| 	"Short multi-line message": { | ||||
| 		input: "I\ncan't\nget\nno\nsatisfaction!", | ||||
| 		splitOutput: []string{ | ||||
| 			"I", | ||||
| 			"can't", | ||||
| 			"get", | ||||
| 			"no", | ||||
| 			"satisfaction!", | ||||
| 		}, | ||||
| 		"Short multi-line message": { | ||||
| 			input: "I\ncan't\nget\nno\nsatisfaction!", | ||||
| 			splitOutput: []string{ | ||||
| 				"I", | ||||
| 				"can't", | ||||
| 				"get", | ||||
| 				"no", | ||||
| 				"satisfaction!", | ||||
| 			}, | ||||
| 			nonSplitOutput: []string{ | ||||
| 				"I", | ||||
| 				"can't", | ||||
| 				"get", | ||||
| 				"no", | ||||
| 				"satisfaction!", | ||||
| 			}, | ||||
| 		nonSplitOutput: []string{ | ||||
| 			"I", | ||||
| 			"can't", | ||||
| 			"get", | ||||
| 			"no", | ||||
| 			"satisfaction!", | ||||
| 		}, | ||||
| 		"Long multi-line message": { | ||||
| 			input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" + | ||||
| 				"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" + | ||||
| 				"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" + | ||||
| 				"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", | ||||
| 			splitOutput: []string{ | ||||
| 				"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>", | ||||
| 				"cing elit, sed do eiusmod tempor incididunt ut <clipped message>", | ||||
| 				" labore et dolore magna aliqua.", | ||||
| 				"Ut enim ad minim veniam, quis nostrud exercita <clipped message>", | ||||
| 				"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>", | ||||
| 				"modo consequat.", | ||||
| 				"Duis aute irure dolor in reprehenderit in volu <clipped message>", | ||||
| 				"ptate velit esse cillum dolore eu fugiat nulla <clipped message>", | ||||
| 				" pariatur.", | ||||
| 				"Excepteur sint occaecat cupidatat non proident <clipped message>", | ||||
| 				", sunt in culpa qui officia deserunt mollit an <clipped message>", | ||||
| 				"im id est laborum.", | ||||
| 			}, | ||||
| 			nonSplitOutput: []string{ | ||||
| 				"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", | ||||
| 				"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", | ||||
| 				"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", | ||||
| 				"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", | ||||
| 			}, | ||||
| 	}, | ||||
| 	"Long multi-line message": { | ||||
| 		input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" + | ||||
| 			"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" + | ||||
| 			"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" + | ||||
| 			"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", | ||||
| 		splitOutput: []string{ | ||||
| 			"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>", | ||||
| 			"cing elit, sed do eiusmod tempor incididunt ut <clipped message>", | ||||
| 			" labore et dolore magna aliqua.", | ||||
| 			"Ut enim ad minim veniam, quis nostrud exercita <clipped message>", | ||||
| 			"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>", | ||||
| 			"modo consequat.", | ||||
| 			"Duis aute irure dolor in reprehenderit in volu <clipped message>", | ||||
| 			"ptate velit esse cillum dolore eu fugiat nulla <clipped message>", | ||||
| 			" pariatur.", | ||||
| 			"Excepteur sint occaecat cupidatat non proident <clipped message>", | ||||
| 			", sunt in culpa qui officia deserunt mollit an <clipped message>", | ||||
| 			"im id est laborum.", | ||||
| 		}, | ||||
| 		"Message ending with new-line.": { | ||||
| 			input:          "Newline ending\n", | ||||
| 			splitOutput:    []string{"Newline ending"}, | ||||
| 			nonSplitOutput: []string{"Newline ending"}, | ||||
| 		nonSplitOutput: []string{ | ||||
| 			"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", | ||||
| 			"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", | ||||
| 			"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", | ||||
| 			"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", | ||||
| 		}, | ||||
| 		"Long message containing UTF-8 multi-byte runes": { | ||||
| 			input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說", | ||||
| 			splitOutput: []string{ | ||||
| 				"不布人個我此而及單石業喜資富下 <clipped message>", | ||||
| 				"我河下日沒一我臺空達的常景便物 <clipped message>", | ||||
| 				"沒為……子大我別名解成?生賣的 <clipped message>", | ||||
| 				"全直黑,我自我結毛分洲了世當, <clipped message>", | ||||
| 				"是政福那是東;斯說", | ||||
| 			}, | ||||
| 			nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"}, | ||||
| 	}, | ||||
| 	"Message ending with new-line.": { | ||||
| 		input:          "Newline ending\n", | ||||
| 		splitOutput:    []string{"Newline ending"}, | ||||
| 		nonSplitOutput: []string{"Newline ending"}, | ||||
| 	}, | ||||
| 	"Long message containing UTF-8 multi-byte runes": { | ||||
| 		input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說", | ||||
| 		splitOutput: []string{ | ||||
| 			"不布人個我此而及單石業喜資富下 <clipped message>", | ||||
| 			"我河下日沒一我臺空達的常景便物 <clipped message>", | ||||
| 			"沒為……子大我別名解成?生賣的 <clipped message>", | ||||
| 			"全直黑,我自我結毛分洲了世當, <clipped message>", | ||||
| 			"是政福那是東;斯說", | ||||
| 		}, | ||||
| 	} | ||||
| ) | ||||
| 		nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"}, | ||||
| 	}, | ||||
| 	"Long message, clip three-byte rune after two bytes": { | ||||
| 		input: "x 人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。", | ||||
| 		splitOutput: []string{ | ||||
| 			"x 人人生而自由,在尊嚴和權利上 <clipped message>", | ||||
| 			"一律平等。 他們都具有理性和良知 <clipped message>", | ||||
| 			",應該以兄弟情誼的精神對待彼此。", | ||||
| 		}, | ||||
| 		nonSplitOutput: []string{"x 人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。"}, | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func TestGetSubLines(t *testing.T) { | ||||
| 	for testname, testcase := range lineSplittingTestCases { | ||||
| 		splitLines := GetSubLines(testcase.input, testLineLength) | ||||
| 		splitLines := GetSubLines(testcase.input, testLineLength, "") | ||||
| 		assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname) | ||||
| 		for _, splitLine := range splitLines { | ||||
| 			byteLength := len([]byte(splitLine)) | ||||
| 			assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength) | ||||
| 		} | ||||
|  | ||||
| 		nonSplitLines := GetSubLines(testcase.input, 0) | ||||
| 		nonSplitLines := GetSubLines(testcase.input, 0, "") | ||||
| 		assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname) | ||||
| 	} | ||||
| } | ||||
| @@ -110,17 +117,122 @@ func TestConvertWebPToPNG(t *testing.T) { | ||||
| 	if os.Getenv("LOCAL_TEST") == "" { | ||||
| 		t.Skip() | ||||
| 	} | ||||
|  | ||||
| 	input, err := ioutil.ReadFile("test.webp") | ||||
| 	if err != nil { | ||||
| 		t.Fail() | ||||
| 	} | ||||
|  | ||||
| 	d := &input | ||||
| 	err = ConvertWebPToPNG(d) | ||||
| 	if err != nil { | ||||
| 		t.Fail() | ||||
| 	} | ||||
| 	err = ioutil.WriteFile("test.png", *d, 0644) | ||||
|  | ||||
| 	err = ioutil.WriteFile("test.png", *d, 0o644) // nolint:gosec | ||||
| 	if err != nil { | ||||
| 		t.Fail() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var clippingOrSplittingTestCases = map[string]struct { | ||||
| 	inputText       string | ||||
| 	clipSplitLength int | ||||
| 	clippingMessage string | ||||
| 	splitMax        int | ||||
| 	expectedOutput  []string | ||||
| }{ | ||||
| 	"Short single-line message, split 3": { | ||||
| 		inputText:       "short", | ||||
| 		clipSplitLength: 20, | ||||
| 		clippingMessage: "?!?!", | ||||
| 		splitMax:        3, | ||||
| 		expectedOutput:  []string{"short"}, | ||||
| 	}, | ||||
| 	"Short single-line message, split 1": { | ||||
| 		inputText:       "short", | ||||
| 		clipSplitLength: 20, | ||||
| 		clippingMessage: "?!?!", | ||||
| 		splitMax:        1, | ||||
| 		expectedOutput:  []string{"short"}, | ||||
| 	}, | ||||
| 	"Short single-line message, split 0": { | ||||
| 		// Mainly check that we don't crash. | ||||
| 		inputText:       "short", | ||||
| 		clipSplitLength: 20, | ||||
| 		clippingMessage: "?!?!", | ||||
| 		splitMax:        0, | ||||
| 		expectedOutput:  []string{"short"}, | ||||
| 	}, | ||||
| 	"Long single-line message, noclip": { | ||||
| 		inputText:       "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", | ||||
| 		clipSplitLength: 50, | ||||
| 		clippingMessage: "?!?!", | ||||
| 		splitMax:        10, | ||||
| 		expectedOutput: []string{ | ||||
| 			"Lorem ipsum dolor sit amet, consectetur adipiscing", | ||||
| 			" elit, sed do eiusmod tempor incididunt ut labore ", | ||||
| 			"et dolore magna aliqua.", | ||||
| 		}, | ||||
| 	}, | ||||
| 	"Long single-line message, noclip tight": { | ||||
| 		inputText:       "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", | ||||
| 		clipSplitLength: 50, | ||||
| 		clippingMessage: "?!?!", | ||||
| 		splitMax:        3, | ||||
| 		expectedOutput: []string{ | ||||
| 			"Lorem ipsum dolor sit amet, consectetur adipiscing", | ||||
| 			" elit, sed do eiusmod tempor incididunt ut labore ", | ||||
| 			"et dolore magna aliqua.", | ||||
| 		}, | ||||
| 	}, | ||||
| 	"Long single-line message, clip custom": { | ||||
| 		inputText:       "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", | ||||
| 		clipSplitLength: 50, | ||||
| 		clippingMessage: "?!?!", | ||||
| 		splitMax:        2, | ||||
| 		expectedOutput: []string{ | ||||
| 			"Lorem ipsum dolor sit amet, consectetur adipiscing", | ||||
| 			" elit, sed do eiusmod tempor incididunt ut lab?!?!", | ||||
| 		}, | ||||
| 	}, | ||||
| 	"Long single-line message, clip built-in": { | ||||
| 		inputText:       "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", | ||||
| 		clipSplitLength: 50, | ||||
| 		clippingMessage: "", | ||||
| 		splitMax:        2, | ||||
| 		expectedOutput: []string{ | ||||
| 			"Lorem ipsum dolor sit amet, consectetur adipiscing", | ||||
| 			" elit, sed do eiusmod tempor inc <clipped message>", | ||||
| 		}, | ||||
| 	}, | ||||
| 	"Short multi-line message": { | ||||
| 		inputText:       "I\ncan't\nget\nno\nsatisfaction!", | ||||
| 		clipSplitLength: 50, | ||||
| 		clippingMessage: "", | ||||
| 		splitMax:        2, | ||||
| 		expectedOutput:  []string{"I\ncan't\nget\nno\nsatisfaction!"}, | ||||
| 	}, | ||||
| 	"Long message containing UTF-8 multi-byte runes": { | ||||
| 		inputText:       "人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。", | ||||
| 		clipSplitLength: 50, | ||||
| 		clippingMessage: "", | ||||
| 		splitMax:        10, | ||||
| 		expectedOutput: []string{ | ||||
| 			"人人生而自由,在尊嚴和權利上一律",  // Note: only 48 bytes! | ||||
| 			"平等。 他們都具有理性和良知,應該", // Note: only 49 bytes! | ||||
| 			"以兄弟情誼的精神對待彼此。", | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func TestClipOrSplitMessage(t *testing.T) { | ||||
| 	for testname, testcase := range clippingOrSplittingTestCases { | ||||
| 		actualOutput := ClipOrSplitMessage(testcase.inputText, testcase.clipSplitLength, testcase.clippingMessage, testcase.splitMax) | ||||
| 		assert.Equalf(t, testcase.expectedOutput, actualOutput, "'%s' testcase should give expected lines with clipping+splitting.", testname) | ||||
| 		for _, splitLine := range testcase.expectedOutput { | ||||
| 			byteLength := len([]byte(splitLine)) | ||||
| 			assert.True(t, byteLength <= testcase.clipSplitLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testname, testcase.clipSplitLength, byteLength) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										35
									
								
								bridge/helper/libtgsconverter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | ||||
| //go:build cgolottie | ||||
|  | ||||
| package helper | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/Benau/tgsconverter/libtgsconverter" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| func CanConvertTgsToX() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // ConvertTgsToX convert input data (which should be tgs format) to any format supported by libtgsconverter | ||||
| func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error { | ||||
| 	options := libtgsconverter.NewConverterOptions() | ||||
| 	options.SetExtension(outputFormat) | ||||
| 	blob, err := libtgsconverter.ImportFromData(*data, options) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to run libtgsconverter.ImportFromData: %s", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	*data = blob | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func SupportsFormat(format string) bool { | ||||
| 	return libtgsconverter.SupportsExtension(format) | ||||
| } | ||||
|  | ||||
| func LottieBackend() string { | ||||
| 	return "libtgsconverter" | ||||
| } | ||||
							
								
								
									
										90
									
								
								bridge/helper/lottie_convert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,90 @@ | ||||
| //go:build !cgolottie | ||||
|  | ||||
| package helper | ||||
|  | ||||
| import ( | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
|  | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| // CanConvertTgsToX Checks whether the external command necessary for ConvertTgsToX works. | ||||
| func CanConvertTgsToX() error { | ||||
| 	// We depend on the fact that `lottie_convert.py --help` has exit status 0. | ||||
| 	// Hyrum's Law predicted this, and Murphy's Law predicts that this will break eventually. | ||||
| 	// However, there is no alternative like `lottie_convert.py --is-properly-installed` | ||||
| 	cmd := exec.Command("lottie_convert.py", "--help") | ||||
| 	return cmd.Run() | ||||
| } | ||||
|  | ||||
| // ConvertTgsToWebP convert input data (which should be tgs format) to WebP format | ||||
| // This relies on an external command, which is ugly, but works. | ||||
| func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error { | ||||
| 	// lottie can't handle input from a pipe, so write to a temporary file: | ||||
| 	tmpInFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-input-*.tgs") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	tmpInFileName := tmpInFile.Name() | ||||
| 	defer func() { | ||||
| 		if removeErr := os.Remove(tmpInFileName); removeErr != nil { | ||||
| 			logger.Errorf("Could not delete temporary (input) file %s: %v", tmpInFileName, removeErr) | ||||
| 		} | ||||
| 	}() | ||||
| 	// lottie can handle writing to a pipe, but there is no way to do that platform-independently. | ||||
| 	// "/dev/stdout" won't work on Windows, and "-" upsets Cairo for some reason. So we need another file: | ||||
| 	tmpOutFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-output-*.data") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	tmpOutFileName := tmpOutFile.Name() | ||||
| 	defer func() { | ||||
| 		if removeErr := os.Remove(tmpOutFileName); removeErr != nil { | ||||
| 			logger.Errorf("Could not delete temporary (output) file %s: %v", tmpOutFileName, removeErr) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	if _, writeErr := tmpInFile.Write(*data); writeErr != nil { | ||||
| 		return writeErr | ||||
| 	} | ||||
| 	// Must close before calling lottie to avoid data races: | ||||
| 	if closeErr := tmpInFile.Close(); closeErr != nil { | ||||
| 		return closeErr | ||||
| 	} | ||||
|  | ||||
| 	// Call lottie to transform: | ||||
| 	cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpInFileName, tmpOutFileName) | ||||
| 	cmd.Stdout = nil | ||||
| 	cmd.Stderr = nil | ||||
| 	// NB: lottie writes progress into to stderr in all cases. | ||||
| 	_, stderr := cmd.Output() | ||||
| 	if stderr != nil { | ||||
| 		// 'stderr' already contains some parts of Stderr, because it was set to 'nil'. | ||||
| 		return stderr | ||||
| 	} | ||||
| 	dataContents, err := ioutil.ReadFile(tmpOutFileName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	*data = dataContents | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func SupportsFormat(format string) bool { | ||||
| 	switch format { | ||||
| 	case "png": | ||||
| 		fallthrough | ||||
| 	case "webp": | ||||
| 		return true | ||||
| 	default: | ||||
| 		return false | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func LottieBackend() string { | ||||
| 	return "lottie_convert.py" | ||||
| } | ||||
							
								
								
									
										32
									
								
								bridge/irc/charset.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,32 @@ | ||||
| package birc | ||||
|  | ||||
| import ( | ||||
| 	"golang.org/x/text/encoding" | ||||
| 	"golang.org/x/text/encoding/japanese" | ||||
| 	"golang.org/x/text/encoding/korean" | ||||
| 	"golang.org/x/text/encoding/simplifiedchinese" | ||||
| 	"golang.org/x/text/encoding/traditionalchinese" | ||||
| 	"golang.org/x/text/encoding/unicode" | ||||
| ) | ||||
|  | ||||
| var encoders = map[string]encoding.Encoding{ | ||||
| 	"utf-8":       unicode.UTF8, | ||||
| 	"iso-2022-jp": japanese.ISO2022JP, | ||||
| 	"big5":        traditionalchinese.Big5, | ||||
| 	"gbk":         simplifiedchinese.GBK, | ||||
| 	"euc-kr":      korean.EUCKR, | ||||
| 	"gb2312":      simplifiedchinese.HZGB2312, | ||||
| 	"shift-jis":   japanese.ShiftJIS, | ||||
| 	"euc-jp":      japanese.EUCJP, | ||||
| 	"gb18030":     simplifiedchinese.GB18030, | ||||
| } | ||||
|  | ||||
| func toUTF8(from string, input string) string { | ||||
| 	enc, ok := encoders[from] | ||||
| 	if !ok { | ||||
| 		return input | ||||
| 	} | ||||
|  | ||||
| 	res, _ := enc.NewDecoder().String(input) | ||||
| 	return res | ||||
| } | ||||
| @@ -11,7 +11,6 @@ import ( | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/lrstanley/girc" | ||||
| 	"github.com/missdeer/golib/ic" | ||||
| 	"github.com/paulrosania/go-charset/charset" | ||||
| 	"github.com/saintfish/chardet" | ||||
|  | ||||
| @@ -24,12 +23,12 @@ func (b *Birc) handleCharset(msg *config.Message) error { | ||||
| 	if b.GetString("Charset") != "" { | ||||
| 		switch b.GetString("Charset") { | ||||
| 		case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": | ||||
| 			msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text) | ||||
| 			msg.Text = toUTF8(b.GetString("Charset"), msg.Text) | ||||
| 		default: | ||||
| 			buf := new(bytes.Buffer) | ||||
| 			w, err := charset.NewWriter(b.GetString("Charset"), buf) | ||||
| 			if err != nil { | ||||
| 				b.Log.Errorf("charset from utf-8 conversion failed: %s", err) | ||||
| 				b.Log.Errorf("charset to utf-8 conversion failed: %s", err) | ||||
| 				return err | ||||
| 			} | ||||
| 			fmt.Fprint(w, msg.Text) | ||||
| @@ -123,8 +122,18 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) { | ||||
| 	i := b.i | ||||
| 	b.Nick = event.Params[0] | ||||
|  | ||||
| 	b.Log.Debug("Clearing handlers before adding in case of BNC reconnect") | ||||
| 	i.Handlers.Clear("PRIVMSG") | ||||
| 	i.Handlers.Clear("CTCP_ACTION") | ||||
| 	i.Handlers.Clear(girc.RPL_TOPICWHOTIME) | ||||
| 	i.Handlers.Clear(girc.NOTICE) | ||||
| 	i.Handlers.Clear("JOIN") | ||||
| 	i.Handlers.Clear("PART") | ||||
| 	i.Handlers.Clear("QUIT") | ||||
| 	i.Handlers.Clear("KICK") | ||||
| 	i.Handlers.Clear("INVITE") | ||||
|  | ||||
| 	i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg) | ||||
| 	i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg) | ||||
| 	i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) | ||||
| 	i.Handlers.AddBg(girc.NOTICE, b.handleNotice) | ||||
| 	i.Handlers.AddBg("JOIN", b.handleJoinPart) | ||||
| @@ -196,7 +205,11 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { | ||||
| 	b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event) | ||||
|  | ||||
| 	// set action event | ||||
| 	if event.IsAction() { | ||||
| 	if ok, ctcp := event.IsCTCP(); ok { | ||||
| 		if ctcp.Command != girc.CTCP_ACTION { | ||||
| 			b.Log.Debugf("dropping user ctcp, command: %s", ctcp.Command) | ||||
| 			return | ||||
| 		} | ||||
| 		rmsg.Event = config.EventUserAction | ||||
| 	} | ||||
|  | ||||
| @@ -227,7 +240,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { | ||||
| 	} | ||||
| 	switch mycharset { | ||||
| 	case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": | ||||
| 		rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text) | ||||
| 		rmsg.Text = toUTF8(b.GetString("Charset"), rmsg.Text) | ||||
| 	default: | ||||
| 		r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text)) | ||||
| 		if err != nil { | ||||
| @@ -244,6 +257,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { | ||||
|  | ||||
| func (b *Birc) handleRunCommands() { | ||||
| 	for _, cmd := range b.GetStringSlice("RunCommands") { | ||||
| 		cmd = strings.ReplaceAll(cmd, "{BOTNICK}", b.Nick) | ||||
| 		if err := b.i.Cmd.SendRaw(cmd); err != nil { | ||||
| 			b.Log.Errorf("RunCommands %s failed: %s", cmd, err) | ||||
| 		} | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package birc | ||||
|  | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"hash/crc32" | ||||
| 	"io/ioutil" | ||||
| @@ -72,6 +73,10 @@ func (b *Birc) Command(msg *config.Message) string { | ||||
| } | ||||
|  | ||||
| func (b *Birc) Connect() error { | ||||
| 	if b.GetBool("UseSASL") && b.GetString("TLSClientCertificate") != "" { | ||||
| 		return errors.New("you can't enable SASL and TLSClientCertificate at the same time") | ||||
| 	} | ||||
|  | ||||
| 	b.Local = make(chan config.Message, b.MessageQueue+10) | ||||
| 	b.Log.Infof("Connecting %s", b.GetString("Server")) | ||||
|  | ||||
| @@ -167,9 +172,9 @@ func (b *Birc) Send(msg config.Message) (string, error) { | ||||
| 	} | ||||
|  | ||||
| 	if b.GetBool("MessageSplit") { | ||||
| 		msgLines = helper.GetSubLines(msg.Text, b.MessageLength) | ||||
| 		msgLines = helper.GetSubLines(msg.Text, b.MessageLength, b.GetString("MessageClipped")) | ||||
| 	} else { | ||||
| 		msgLines = helper.GetSubLines(msg.Text, 0) | ||||
| 		msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped")) | ||||
| 	} | ||||
| 	for i := range msgLines { | ||||
| 		if len(b.Local) >= b.MessageQueue { | ||||
| @@ -271,8 +276,11 @@ func (b *Birc) getClient() (*girc.Client, error) { | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	user := b.GetString("UserName") | ||||
| 	if user == "" { | ||||
| 		user = b.GetString("Nick") | ||||
| 	} | ||||
| 	// fix strict user handling of girc | ||||
| 	user := b.GetString("Nick") | ||||
| 	for !girc.IsValidUser(user) { | ||||
| 		if len(user) == 1 || len(user) == 0 { | ||||
| 			user = "matterbridge" | ||||
| @@ -280,6 +288,10 @@ func (b *Birc) getClient() (*girc.Client, error) { | ||||
| 		} | ||||
| 		user = user[1:] | ||||
| 	} | ||||
| 	realName := b.GetString("RealName") | ||||
| 	if realName == "" { | ||||
| 		realName = b.GetString("Nick") | ||||
| 	} | ||||
|  | ||||
| 	debug := ioutil.Discard | ||||
| 	if b.GetInt("DebugLevel") == 2 { | ||||
| @@ -293,15 +305,21 @@ func (b *Birc) getClient() (*girc.Client, error) { | ||||
|  | ||||
| 	b.Log.Debugf("setting pingdelay to %s", pingDelay) | ||||
|  | ||||
| 	tlsConfig, err := b.getTLSConfig() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	i := girc.New(girc.Config{ | ||||
| 		Server:     server, | ||||
| 		ServerPass: b.GetString("Password"), | ||||
| 		Port:       port, | ||||
| 		Nick:       b.GetString("Nick"), | ||||
| 		User:       user, | ||||
| 		Name:       b.GetString("Nick"), | ||||
| 		Name:       realName, | ||||
| 		SSL:        b.GetBool("UseTLS"), | ||||
| 		TLSConfig:  &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec | ||||
| 		Bind:       b.GetString("Bind"), | ||||
| 		TLSConfig:  tlsConfig, | ||||
| 		PingDelay:  pingDelay, | ||||
| 		// skip gIRC internal rate limiting, since we have our own throttling | ||||
| 		AllowFlood:    true, | ||||
| @@ -316,12 +334,16 @@ func (b *Birc) endNames(client *girc.Client, event girc.Event) { | ||||
| 	sort.Strings(b.names[channel]) | ||||
| 	maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow() | ||||
| 	for len(b.names[channel]) > maxNamesPerPost { | ||||
| 		b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]), | ||||
| 			Channel: channel, Account: b.Account} | ||||
| 		b.Remote <- config.Message{ | ||||
| 			Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]), | ||||
| 			Channel: channel, Account: b.Account, | ||||
| 		} | ||||
| 		b.names[channel] = b.names[channel][maxNamesPerPost:] | ||||
| 	} | ||||
| 	b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel]), | ||||
| 		Channel: channel, Account: b.Account} | ||||
| 	b.Remote <- config.Message{ | ||||
| 		Username: b.Nick, Text: b.formatnicks(b.names[channel]), | ||||
| 		Channel: channel, Account: b.Account, | ||||
| 	} | ||||
| 	b.names[channel] = nil | ||||
| 	b.i.Handlers.Clear(girc.RPL_NAMREPLY) | ||||
| 	b.i.Handlers.Clear(girc.RPL_ENDOFNAMES) | ||||
| @@ -340,8 +362,10 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool { | ||||
| 		return true | ||||
| 	} | ||||
| 	// don't forward message from ourself | ||||
| 	if event.Source.Name == b.Nick { | ||||
| 		return true | ||||
| 	if event.Source != nil { | ||||
| 		if event.Source.Name == b.Nick { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	// don't forward messages we sent via RELAYMSG | ||||
| 	if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick { | ||||
| @@ -369,3 +393,23 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) { | ||||
| func (b *Birc) formatnicks(nicks []string) string { | ||||
| 	return strings.Join(nicks, ", ") + " currently on IRC" | ||||
| } | ||||
|  | ||||
| func (b *Birc) getTLSConfig() (*tls.Config, error) { | ||||
| 	server, _, _ := net.SplitHostPort(b.GetString("server")) | ||||
|  | ||||
| 	tlsConfig := &tls.Config{ | ||||
| 		InsecureSkipVerify: b.GetBool("skiptlsverify"), //nolint:gosec | ||||
| 		ServerName:         server, | ||||
| 	} | ||||
|  | ||||
| 	if filename := b.GetString("TLSClientCertificate"); filename != "" { | ||||
| 		cert, err := tls.LoadX509KeyPair(filename, filename) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		tlsConfig.Certificates = []tls.Certificate{cert} | ||||
| 	} | ||||
|  | ||||
| 	return tlsConfig, nil | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import ( | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	matrix "github.com/matrix-org/gomatrix" | ||||
| 	matrix "github.com/matterbridge/gomatrix" | ||||
| ) | ||||
|  | ||||
| func newMatrixUsername(username string) *matrixUsername { | ||||
| @@ -51,7 +51,7 @@ func interface2Struct(in interface{}, out interface{}) error { | ||||
| 	return json.Unmarshal(jsonObj, out) | ||||
| } | ||||
|  | ||||
| // getDisplayName retrieves the displayName for mxid, querying the homserver if the mxid is not in the cache. | ||||
| // getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache. | ||||
| func (b *Bmatrix) getDisplayName(mxid string) string { | ||||
| 	if b.GetBool("UseUserName") { | ||||
| 		return mxid[1:] | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import ( | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	matrix "github.com/matrix-org/gomatrix" | ||||
| 	matrix "github.com/matterbridge/gomatrix" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| @@ -48,8 +48,10 @@ type matrixUsername struct { | ||||
|  | ||||
| // SubTextMessage represents the new content of the message in edit messages. | ||||
| type SubTextMessage struct { | ||||
| 	MsgType string `json:"msgtype"` | ||||
| 	Body    string `json:"body"` | ||||
| 	MsgType       string `json:"msgtype"` | ||||
| 	Body          string `json:"body"` | ||||
| 	FormattedBody string `json:"formatted_body,omitempty"` | ||||
| 	Format        string `json:"format,omitempty"` | ||||
| } | ||||
|  | ||||
| // MessageRelation explains how the current message relates to a previous message. | ||||
| @@ -65,6 +67,19 @@ type EditedMessage struct { | ||||
| 	matrix.TextMessage | ||||
| } | ||||
|  | ||||
| type InReplyToRelationContent struct { | ||||
| 	EventID string `json:"event_id"` | ||||
| } | ||||
|  | ||||
| type InReplyToRelation struct { | ||||
| 	InReplyTo InReplyToRelationContent `json:"m.in_reply_to"` | ||||
| } | ||||
|  | ||||
| type ReplyMessage struct { | ||||
| 	RelatedTo InReplyToRelation `json:"m.relates_to"` | ||||
| 	matrix.TextMessage | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	b := &Bmatrix{Config: cfg} | ||||
| 	b.RoomMap = make(map[string]string) | ||||
| @@ -75,22 +90,33 @@ func New(cfg *bridge.Config) bridge.Bridger { | ||||
| func (b *Bmatrix) Connect() error { | ||||
| 	var err error | ||||
| 	b.Log.Infof("Connecting %s", b.GetString("Server")) | ||||
| 	b.mc, err = matrix.NewClient(b.GetString("Server"), "", "") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	if b.GetString("MxID") != "" && b.GetString("Token") != "" { | ||||
| 		b.mc, err = matrix.NewClient( | ||||
| 			b.GetString("Server"), b.GetString("MxID"), b.GetString("Token"), | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		b.UserID = b.GetString("MxID") | ||||
| 		b.Log.Info("Using existing Matrix credentials") | ||||
| 	} else { | ||||
| 		b.mc, err = matrix.NewClient(b.GetString("Server"), "", "") | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		resp, err := b.mc.Login(&matrix.ReqLogin{ | ||||
| 			Type:       "m.login.password", | ||||
| 			User:       b.GetString("Login"), | ||||
| 			Password:   b.GetString("Password"), | ||||
| 			Identifier: matrix.NewUserIdentifier(b.GetString("Login")), | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		b.mc.SetCredentials(resp.UserID, resp.AccessToken) | ||||
| 		b.UserID = resp.UserID | ||||
| 		b.Log.Info("Connection succeeded") | ||||
| 	} | ||||
| 	resp, err := b.mc.Login(&matrix.ReqLogin{ | ||||
| 		Type:       "m.login.password", | ||||
| 		User:       b.GetString("Login"), | ||||
| 		Password:   b.GetString("Password"), | ||||
| 		Identifier: matrix.NewUserIdentifier(b.GetString("Login")), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	b.mc.SetCredentials(resp.UserID, resp.AccessToken) | ||||
| 	b.UserID = resp.UserID | ||||
| 	b.Log.Info("Connection succeeded") | ||||
| 	go b.handlematrix() | ||||
| 	return nil | ||||
| } | ||||
| @@ -122,12 +148,43 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||
|  | ||||
| 	username := newMatrixUsername(msg.Username) | ||||
|  | ||||
| 	body := username.plain + msg.Text | ||||
| 	formattedBody := username.formatted + helper.ParseMarkdown(msg.Text) | ||||
|  | ||||
| 	if b.GetBool("SpoofUsername") { | ||||
| 		// https://spec.matrix.org/v1.3/client-server-api/#mroommember | ||||
| 		type stateMember struct { | ||||
| 			AvatarURL   string `json:"avatar_url,omitempty"` | ||||
| 			DisplayName string `json:"displayname"` | ||||
| 			Membership  string `json:"membership"` | ||||
| 		} | ||||
|  | ||||
| 		// TODO: reset username afterwards with DisplayName: null ? | ||||
| 		m := stateMember{ | ||||
| 			AvatarURL:   "", | ||||
| 			DisplayName: username.plain, | ||||
| 			Membership:  "join", | ||||
| 		} | ||||
|  | ||||
| 		_, err := b.mc.SendStateEvent(channel, "m.room.member", b.UserID, m) | ||||
| 		if err == nil { | ||||
| 			body = msg.Text | ||||
| 			formattedBody = helper.ParseMarkdown(msg.Text) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Make a action /me of the message | ||||
| 	if msg.Event == config.EventUserAction { | ||||
| 		m := matrix.TextMessage{ | ||||
| 			MsgType:       "m.emote", | ||||
| 			Body:          username.plain + msg.Text, | ||||
| 			FormattedBody: username.formatted + msg.Text, | ||||
| 			Body:          body, | ||||
| 			FormattedBody: formattedBody, | ||||
| 			Format:        "org.matrix.custom.html", | ||||
| 		} | ||||
|  | ||||
| 		if b.GetBool("HTMLDisable") { | ||||
| 			m.Format = "" | ||||
| 			m.FormattedBody = "" | ||||
| 		} | ||||
|  | ||||
| 		msgID := "" | ||||
| @@ -190,20 +247,29 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||
|  | ||||
| 	// Edit message if we have an ID | ||||
| 	if msg.ID != "" { | ||||
| 		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 := EditedMessage{ | ||||
| 			TextMessage: matrix.TextMessage{ | ||||
| 				Body:          body, | ||||
| 				MsgType:       "m.text", | ||||
| 				Format:        "org.matrix.custom.html", | ||||
| 				FormattedBody: formattedBody, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		rmsg.NewContent = SubTextMessage{ | ||||
| 			Body:    rmsg.TextMessage.Body, | ||||
| 			MsgType: "m.text", | ||||
| 			Body:          rmsg.TextMessage.Body, | ||||
| 			FormattedBody: rmsg.TextMessage.FormattedBody, | ||||
| 			Format:        rmsg.TextMessage.Format, | ||||
| 			MsgType:       "m.text", | ||||
| 		} | ||||
|  | ||||
| 		if b.GetBool("HTMLDisable") { | ||||
| 			rmsg.TextMessage.Format = "" | ||||
| 			rmsg.TextMessage.FormattedBody = "" | ||||
| 			rmsg.NewContent.Format = "" | ||||
| 			rmsg.NewContent.FormattedBody = "" | ||||
| 		} | ||||
|  | ||||
| 		rmsg.RelatedTo = MessageRelation{ | ||||
| 			EventID: msg.ID, | ||||
| 			Type:    "m.replace", | ||||
| @@ -225,8 +291,52 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||
| 	if msg.Event == config.EventJoinLeave { | ||||
| 		m := matrix.TextMessage{ | ||||
| 			MsgType:       "m.notice", | ||||
| 			Body:          username.plain + msg.Text, | ||||
| 			FormattedBody: username.formatted + msg.Text, | ||||
| 			Body:          body, | ||||
| 			FormattedBody: formattedBody, | ||||
| 			Format:        "org.matrix.custom.html", | ||||
| 		} | ||||
|  | ||||
| 		if b.GetBool("HTMLDisable") { | ||||
| 			m.Format = "" | ||||
| 			m.FormattedBody = "" | ||||
| 		} | ||||
|  | ||||
| 		var ( | ||||
| 			resp *matrix.RespSendEvent | ||||
| 			err  error | ||||
| 		) | ||||
|  | ||||
| 		err = b.retry(func() error { | ||||
| 			resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m) | ||||
|  | ||||
| 			return err | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		return resp.EventID, err | ||||
| 	} | ||||
|  | ||||
| 	if msg.ParentValid() { | ||||
| 		m := ReplyMessage{ | ||||
| 			TextMessage: matrix.TextMessage{ | ||||
| 				MsgType:       "m.text", | ||||
| 				Body:          body, | ||||
| 				FormattedBody: formattedBody, | ||||
| 				Format:        "org.matrix.custom.html", | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		if b.GetBool("HTMLDisable") { | ||||
| 			m.TextMessage.Format = "" | ||||
| 			m.TextMessage.FormattedBody = "" | ||||
| 		} | ||||
|  | ||||
| 		m.RelatedTo = InReplyToRelation{ | ||||
| 			InReplyTo: InReplyToRelationContent{ | ||||
| 				EventID: msg.ParentID, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		var ( | ||||
| @@ -253,7 +363,7 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||
| 		) | ||||
|  | ||||
| 		err = b.retry(func() error { | ||||
| 			resp, err = b.mc.SendText(channel, username.plain+msg.Text) | ||||
| 			resp, err = b.mc.SendText(channel, body) | ||||
|  | ||||
| 			return err | ||||
| 		}) | ||||
| @@ -271,8 +381,7 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||
| 	) | ||||
|  | ||||
| 	err = b.retry(func() error { | ||||
| 		resp, err = b.mc.SendFormattedText(channel, username.plain+msg.Text, | ||||
| 			username.formatted+helper.ParseMarkdown(msg.Text)) | ||||
| 		resp, err = b.mc.SendFormattedText(channel, body, formattedBody) | ||||
|  | ||||
| 		return err | ||||
| 	}) | ||||
| @@ -290,6 +399,9 @@ func (b *Bmatrix) handlematrix() { | ||||
| 	syncer.OnEventType("m.room.member", b.handleMemberChange) | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			if b == nil { | ||||
| 				return | ||||
| 			} | ||||
| 			if err := b.mc.Sync(); err != nil { | ||||
| 				b.Log.Println("Sync() returned ", err) | ||||
| 			} | ||||
| @@ -327,6 +439,38 @@ func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool { | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool { | ||||
| 	relationInterface, present := ev.Content["m.relates_to"] | ||||
| 	if !present { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	var relation InReplyToRelation | ||||
| 	if err := interface2Struct(relationInterface, &relation); err != nil { | ||||
| 		// probably fine | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	body := rmsg.Text | ||||
|  | ||||
| 	if !b.GetBool("keepquotedreply") { | ||||
| 		for strings.HasPrefix(body, "> ") { | ||||
| 			lineIdx := strings.IndexRune(body, '\n') | ||||
| 			if lineIdx == -1 { | ||||
| 				body = "" | ||||
| 			} else { | ||||
| 				body = body[(lineIdx + 1):] | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	rmsg.Text = body | ||||
| 	rmsg.ParentID = relation.InReplyTo.EventID | ||||
| 	b.Remote <- rmsg | ||||
|  | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (b *Bmatrix) handleMemberChange(ev *matrix.Event) { | ||||
| 	// Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information | ||||
| 	if ev.Content["membership"] == "join" { | ||||
| @@ -357,13 +501,6 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { | ||||
| 			Avatar:   b.getAvatarURL(ev.Sender), | ||||
| 		} | ||||
|  | ||||
| 		// Text must be a string | ||||
| 		if rmsg.Text, ok = ev.Content["body"].(string); !ok { | ||||
| 			b.Log.Errorf("Content[body] is not a string: %T\n%#v", | ||||
| 				ev.Content["body"], ev.Content) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Remove homeserver suffix if configured | ||||
| 		if b.GetBool("NoHomeServerSuffix") { | ||||
| 			re := regexp.MustCompile("(.*?):.*") | ||||
| @@ -379,6 +516,13 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Text must be a string | ||||
| 		if rmsg.Text, ok = ev.Content["body"].(string); !ok { | ||||
| 			b.Log.Errorf("Content[body] is not a string: %T\n%#v", | ||||
| 				ev.Content["body"], ev.Content) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Do we have a /me action | ||||
| 		if ev.Content["msgtype"].(string) == "m.emote" { | ||||
| 			rmsg.Event = config.EventUserAction | ||||
| @@ -389,6 +533,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Is it a reply? | ||||
| 		if b.handleReply(ev, rmsg) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Do we have attachments | ||||
| 		if b.containsAttachment(ev.Content) { | ||||
| 			err := b.handleDownloadFile(&rmsg, ev.Content) | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| package bmattermost | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/42wim/matterbridge/matterclient" | ||||
| 	"github.com/mattermost/mattermost-server/v5/model" | ||||
| 	"github.com/matterbridge/matterclient" | ||||
| 	"github.com/mattermost/mattermost/server/public/model" | ||||
| ) | ||||
|  | ||||
| // handleDownloadAvatar downloads the avatar of userid from channel | ||||
| @@ -21,12 +23,17 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) { | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 	} | ||||
| 	if _, ok := b.avatarMap[userid]; !ok { | ||||
| 		data, resp := b.mc.Client.GetProfileImage(userid, "") | ||||
| 		if resp.Error != nil { | ||||
| 			b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error) | ||||
| 		var ( | ||||
| 			data []byte | ||||
| 			err  error | ||||
| 		) | ||||
| 		data, _, err = b.mc.Client.GetProfileImage(context.TODO(), userid, "") | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err) | ||||
| 			return | ||||
| 		} | ||||
| 		err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General) | ||||
|  | ||||
| 		err = helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General) | ||||
| 		if err != nil { | ||||
| 			b.Log.Error(err) | ||||
| 			return | ||||
| @@ -36,20 +43,20 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // handleDownloadFile handles file download | ||||
| //nolint:wrapcheck | ||||
| func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error { | ||||
| 	url, _ := b.mc.Client.GetFileLink(id) | ||||
| 	finfo, resp := b.mc.Client.GetFileInfo(id) | ||||
| 	if resp.Error != nil { | ||||
| 		return resp.Error | ||||
| 	} | ||||
| 	err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General) | ||||
| 	url, _, _ := b.mc.Client.GetFileLink(context.TODO(), id) | ||||
| 	finfo, _, err := b.mc.Client.GetFileInfo(context.TODO(), id) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	data, resp := b.mc.Client.DownloadFile(id, true) | ||||
| 	if resp.Error != nil { | ||||
| 		return resp.Error | ||||
| 	err = helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	data, _, err := b.mc.Client.DownloadFile(context.TODO(), id, true) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General) | ||||
| 	return nil | ||||
| @@ -86,18 +93,24 @@ func (b *Bmattermost) handleMatter() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| //nolint:cyclop | ||||
| func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { | ||||
| 	for message := range b.mc.MessageChan { | ||||
| 		b.Log.Debugf("%#v", message.Raw.Data) | ||||
| 		b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType()) | ||||
|  | ||||
| 		if b.skipMessage(message) { | ||||
| 			b.Log.Debugf("Skipped message: %#v", message) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		channelName := b.getChannelName(message.Post.ChannelId) | ||||
| 		if channelName == "" { | ||||
| 			channelName = message.Channel | ||||
| 		} | ||||
|  | ||||
| 		// only download avatars if we have a place to upload them (configured mediaserver) | ||||
| 		if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" { | ||||
| 			b.handleDownloadAvatar(message.UserID, message.Channel) | ||||
| 			b.handleDownloadAvatar(message.UserID, channelName) | ||||
| 		} | ||||
|  | ||||
| 		b.Log.Debugf("== Receiving event %#v", message) | ||||
| @@ -105,7 +118,7 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { | ||||
| 		rmsg := &config.Message{ | ||||
| 			Username: message.Username, | ||||
| 			UserID:   message.UserID, | ||||
| 			Channel:  message.Channel, | ||||
| 			Channel:  channelName, | ||||
| 			Text:     message.Text, | ||||
| 			ID:       message.Post.Id, | ||||
| 			ParentID: message.Post.RootId, // ParentID is obsolete with mattermost | ||||
| @@ -116,11 +129,11 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { | ||||
| 		b.handleProps(rmsg, message) | ||||
|  | ||||
| 		// create a text for bridges that don't support native editing | ||||
| 		if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") { | ||||
| 		if message.Raw.EventType() == model.WebsocketEventPostEdited && !b.GetBool("EditDisable") { | ||||
| 			rmsg.Text = message.Text + b.GetString("EditSuffix") | ||||
| 		} | ||||
|  | ||||
| 		if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED { | ||||
| 		if message.Raw.EventType() == model.WebsocketEventPostDeleted { | ||||
| 			rmsg.Event = config.EventMsgDelete | ||||
| 		} | ||||
|  | ||||
| @@ -132,8 +145,10 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { | ||||
| 		} | ||||
|  | ||||
| 		// Use nickname instead of username if defined | ||||
| 		if nick := b.mc.GetNickName(rmsg.UserID); nick != "" { | ||||
| 			rmsg.Username = nick | ||||
| 		if !b.GetBool("useusername") { | ||||
| 			if nick := b.mc.GetNickName(rmsg.UserID); nick != "" { | ||||
| 				rmsg.Username = nick | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		messages <- rmsg | ||||
| @@ -144,6 +159,7 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) { | ||||
| 	for { | ||||
| 		message := b.mh.Receive() | ||||
| 		b.Log.Debugf("Receiving from matterhook %#v", message) | ||||
|  | ||||
| 		messages <- &config.Message{ | ||||
| 			UserID:   message.UserID, | ||||
| 			Username: message.UserName, | ||||
| @@ -153,11 +169,10 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // handleUploadFile handles native upload of files | ||||
| func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) { | ||||
| 	var err error | ||||
| 	var res, id string | ||||
| 	channelID := b.mc.GetChannelId(msg.Channel, b.TeamID) | ||||
| 	channelID := b.getChannelID(msg.Channel) | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi := f.(config.FileInfo) | ||||
| 		id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name) | ||||
| @@ -173,6 +188,7 @@ func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) { | ||||
| 	return res, err | ||||
| } | ||||
|  | ||||
| //nolint:forcetypeassert | ||||
| func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) { | ||||
| 	props := message.Post.Props | ||||
| 	if props == nil { | ||||
| @@ -183,16 +199,18 @@ func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Me | ||||
| 	} | ||||
| 	if _, ok := props["attachments"].([]interface{}); ok { | ||||
| 		rmsg.Extra["attachments"] = props["attachments"].([]interface{}) | ||||
| 		if rmsg.Text == "" { | ||||
| 			for _, attachment := range rmsg.Extra["attachments"] { | ||||
| 				attach := attachment.(map[string]interface{}) | ||||
| 				if attach["text"].(string) != "" { | ||||
| 					rmsg.Text += attach["text"].(string) | ||||
| 					continue | ||||
| 				} | ||||
| 				if attach["fallback"].(string) != "" { | ||||
| 					rmsg.Text += attach["fallback"].(string) | ||||
| 				} | ||||
| 		if rmsg.Text != "" { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		for _, attachment := range rmsg.Extra["attachments"] { | ||||
| 			attach := attachment.(map[string]interface{}) | ||||
| 			if attach["text"].(string) != "" { | ||||
| 				rmsg.Text += attach["text"].(string) | ||||
| 				continue | ||||
| 			} | ||||
| 			if attach["fallback"].(string) != "" { | ||||
| 				rmsg.Text += attach["fallback"].(string) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -1,13 +1,14 @@ | ||||
| package bmattermost | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/42wim/matterbridge/matterclient" | ||||
| 	"github.com/42wim/matterbridge/matterhook" | ||||
| 	"github.com/mattermost/mattermost-server/v5/model" | ||||
| 	"github.com/matterbridge/matterclient" | ||||
| 	"github.com/mattermost/mattermost/server/public/model" | ||||
| ) | ||||
|  | ||||
| func (b *Bmattermost) doConnectWebhookBind() error { | ||||
| @@ -15,8 +16,10 @@ func (b *Bmattermost) doConnectWebhookBind() error { | ||||
| 	case b.GetString("WebhookURL") != "": | ||||
| 		b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") | ||||
| 		b.mh = matterhook.New(b.GetString("WebhookURL"), | ||||
| 			matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | ||||
| 				BindAddress: b.GetString("WebhookBindAddress")}) | ||||
| 			matterhook.Config{ | ||||
| 				InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | ||||
| 				BindAddress:        b.GetString("WebhookBindAddress"), | ||||
| 			}) | ||||
| 	case b.GetString("Token") != "": | ||||
| 		b.Log.Info("Connecting using token (sending)") | ||||
| 		err := b.apiLogin() | ||||
| @@ -32,8 +35,10 @@ func (b *Bmattermost) doConnectWebhookBind() error { | ||||
| 	default: | ||||
| 		b.Log.Info("Connecting using webhookbindaddress (receiving)") | ||||
| 		b.mh = matterhook.New(b.GetString("WebhookURL"), | ||||
| 			matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | ||||
| 				BindAddress: b.GetString("WebhookBindAddress")}) | ||||
| 			matterhook.Config{ | ||||
| 				InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | ||||
| 				BindAddress:        b.GetString("WebhookBindAddress"), | ||||
| 			}) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @@ -41,8 +46,10 @@ func (b *Bmattermost) doConnectWebhookBind() error { | ||||
| func (b *Bmattermost) doConnectWebhookURL() error { | ||||
| 	b.Log.Info("Connecting using webhookurl (sending)") | ||||
| 	b.mh = matterhook.New(b.GetString("WebhookURL"), | ||||
| 		matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | ||||
| 			DisableServer: true}) | ||||
| 		matterhook.Config{ | ||||
| 			InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | ||||
| 			DisableServer:      true, | ||||
| 		}) | ||||
| 	if b.GetString("Token") != "" { | ||||
| 		b.Log.Info("Connecting using token (receiving)") | ||||
| 		err := b.apiLogin() | ||||
| @@ -59,13 +66,14 @@ func (b *Bmattermost) doConnectWebhookURL() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| //nolint:wrapcheck | ||||
| func (b *Bmattermost) apiLogin() error { | ||||
| 	password := b.GetString("Password") | ||||
| 	if b.GetString("Token") != "" { | ||||
| 		password = "token=" + b.GetString("Token") | ||||
| 	} | ||||
|  | ||||
| 	b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server")) | ||||
| 	b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"), "") | ||||
| 	if b.GetBool("debug") { | ||||
| 		b.mc.SetLogLevel("debug") | ||||
| 	} | ||||
| @@ -73,14 +81,13 @@ func (b *Bmattermost) apiLogin() error { | ||||
| 	b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck") | ||||
| 	b.mc.NoTLS = b.GetBool("NoTLS") | ||||
| 	b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server")) | ||||
| 	err := b.mc.Login() | ||||
| 	if err != nil { | ||||
|  | ||||
| 	if err := b.mc.Login(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Info("Connection succeeded") | ||||
| 	b.TeamID = b.mc.GetTeamId() | ||||
| 	go b.mc.WsReceiver() | ||||
| 	go b.mc.StatusLoop() | ||||
| 	b.TeamID = b.mc.GetTeamID() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @@ -113,6 +120,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) { | ||||
| 	if b.GetBool("PrefixMessagesWithNick") { | ||||
| 		msg.Text = msg.Username + msg.Text | ||||
| 	} | ||||
|  | ||||
| 	if msg.Extra != nil { | ||||
| 		// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE | ||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | ||||
| @@ -136,7 +144,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) { | ||||
| 			for _, f := range msg.Extra["file"] { | ||||
| 				fi := f.(config.FileInfo) | ||||
| 				if fi.URL != "" { | ||||
| 					msg.Text += fi.URL | ||||
| 					msg.Text += " " + fi.URL | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| @@ -163,19 +171,37 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) { | ||||
| } | ||||
|  | ||||
| // skipMessages returns true if this message should not be handled | ||||
| // | ||||
| //nolint:gocyclo,cyclop | ||||
| func (b *Bmattermost) skipMessage(message *matterclient.Message) bool { | ||||
| 	// Handle join/leave | ||||
| 	if message.Type == "system_join_leave" || | ||||
| 		message.Type == "system_join_channel" || | ||||
| 		message.Type == "system_leave_channel" { | ||||
| 	skipJoinMessageTypes := map[string]struct{}{ | ||||
| 		"system_join_leave":          {}, // deprecated for system_add_to_channel | ||||
| 		"system_leave_channel":       {}, // deprecated for system_remove_from_channel | ||||
| 		"system_join_channel":        {}, | ||||
| 		"system_add_to_channel":      {}, | ||||
| 		"system_remove_from_channel": {}, | ||||
| 		"system_add_to_team":         {}, | ||||
| 		"system_remove_from_team":    {}, | ||||
| 	} | ||||
|  | ||||
| 	// dirty hack to efficiently check if this element is in the map without writing a contains func | ||||
| 	// can be replaced with native slice.contains with go 1.21 | ||||
| 	if _, ok := skipJoinMessageTypes[message.Type]; ok { | ||||
| 		if b.GetBool("nosendjoinpart") { | ||||
| 			return true | ||||
| 		} | ||||
|  | ||||
| 		channelName := b.getChannelName(message.Post.ChannelId) | ||||
| 		if channelName == "" { | ||||
| 			channelName = message.Channel | ||||
| 		} | ||||
|  | ||||
| 		b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account) | ||||
| 		b.Remote <- config.Message{ | ||||
| 			Username: "system", | ||||
| 			Text:     message.Text, | ||||
| 			Channel:  message.Channel, | ||||
| 			Channel:  channelName, | ||||
| 			Account:  b.Account, | ||||
| 			Event:    config.EventJoinLeave, | ||||
| 		} | ||||
| @@ -183,7 +209,7 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool { | ||||
| 	} | ||||
|  | ||||
| 	// Handle edited messages | ||||
| 	if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") { | ||||
| 	if (message.Raw.EventType() == model.WebsocketEventPostEdited) && b.GetBool("EditDisable") { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| @@ -196,13 +222,14 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool { | ||||
| 	// Ignore messages sent from matterbridge | ||||
| 	if message.Post.Props != nil { | ||||
| 		if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok { | ||||
| 			b.Log.Debugf("sent by matterbridge, ignoring") | ||||
| 			b.Log.Debug("sent by matterbridge, ignoring") | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Ignore messages sent from a user logged in as the bot | ||||
| 	if b.mc.User.Username == message.Username { | ||||
| 		b.Log.Debug("message from same user as bot, ignoring") | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| @@ -212,14 +239,56 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool { | ||||
| 	} | ||||
|  | ||||
| 	// ignore messages from other teams than ours | ||||
| 	if message.Raw.Data["team_id"].(string) != b.TeamID { | ||||
| 	if message.Raw.GetData()["team_id"].(string) != b.TeamID { | ||||
| 		b.Log.Debug("message from other team, ignoring") | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	// only handle posted, edited or deleted events | ||||
| 	if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED || | ||||
| 		message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) { | ||||
| 	if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model.WebsocketEventPostEdited || | ||||
| 		message.Raw.EventType() == model.WebsocketEventPostDeleted) { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (b *Bmattermost) getVersion() string { | ||||
| 	proto := "https" | ||||
|  | ||||
| 	if b.GetBool("notls") { | ||||
| 		proto = "http" | ||||
| 	} | ||||
|  | ||||
| 	resp, err := http.Get(proto + "://" + b.GetString("server")) | ||||
| 	if err != nil { | ||||
| 		b.Log.Error("failed getting version") | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	return resp.Header.Get("X-Version-Id") | ||||
| } | ||||
|  | ||||
| func (b *Bmattermost) getChannelID(name string) string { | ||||
| 	idcheck := strings.Split(name, "ID:") | ||||
| 	if len(idcheck) > 1 { | ||||
| 		return idcheck[1] | ||||
| 	} | ||||
|  | ||||
| 	return b.mc.GetChannelID(name, b.TeamID) | ||||
| } | ||||
|  | ||||
| func (b *Bmattermost) getChannelName(id string) string { | ||||
| 	b.channelsMutex.RLock() | ||||
| 	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 | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return "" | ||||
| } | ||||
|   | ||||
| @@ -1,31 +1,44 @@ | ||||
| package bmattermost | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/42wim/matterbridge/matterclient" | ||||
| 	"github.com/42wim/matterbridge/matterhook" | ||||
| 	"github.com/matterbridge/matterclient" | ||||
| 	"github.com/rs/xid" | ||||
| ) | ||||
|  | ||||
| type Bmattermost struct { | ||||
| 	mh     *matterhook.Client | ||||
| 	mc     *matterclient.MMClient | ||||
| 	mc     *matterclient.Client | ||||
| 	v6     bool | ||||
| 	uuid   string | ||||
| 	TeamID string | ||||
| 	*bridge.Config | ||||
| 	avatarMap map[string]string | ||||
| 	avatarMap      map[string]string | ||||
| 	channelsMutex  sync.RWMutex | ||||
| 	channelInfoMap map[string]*config.ChannelInfo | ||||
| } | ||||
|  | ||||
| const mattermostPlugin = "mattermost.plugin" | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	b := &Bmattermost{Config: cfg, avatarMap: make(map[string]string)} | ||||
| 	b := &Bmattermost{ | ||||
| 		Config:         cfg, | ||||
| 		avatarMap:      make(map[string]string), | ||||
| 		channelInfoMap: make(map[string]*config.ChannelInfo), | ||||
| 	} | ||||
|  | ||||
| 	b.v6 = b.GetBool("v6") | ||||
| 	b.uuid = xid.New().String() | ||||
|  | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| @@ -37,6 +50,13 @@ func (b *Bmattermost) Connect() error { | ||||
| 	if b.Account == mattermostPlugin { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if strings.HasPrefix(b.getVersion(), "6.") || strings.HasPrefix(b.getVersion(), "7.") { | ||||
| 		if !b.v6 { | ||||
| 			b.v6 = true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if b.GetString("WebhookBindAddress") != "" { | ||||
| 		if err := b.doConnectWebhookBind(); err != nil { | ||||
| 			return err | ||||
| @@ -60,6 +80,7 @@ func (b *Bmattermost) Connect() error { | ||||
| 		go b.handleMatter() | ||||
| 	case b.GetString("Login") != "": | ||||
| 		b.Log.Info("Connecting using login/password (sending and receiving)") | ||||
| 		b.Log.Infof("Using mattermost v6 methods: %t", b.v6) | ||||
| 		err := b.apiLogin() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| @@ -81,14 +102,21 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	if b.Account == mattermostPlugin { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	b.channelsMutex.Lock() | ||||
| 	b.channelInfoMap[channel.ID] = &channel | ||||
| 	b.channelsMutex.Unlock() | ||||
|  | ||||
| 	// we can only join channels using the API | ||||
| 	if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" { | ||||
| 		id := b.mc.GetChannelId(channel.Name, b.TeamID) | ||||
| 		id := b.getChannelID(channel.Name) | ||||
| 		if id == "" { | ||||
| 			return fmt.Errorf("Could not find channel ID for channel %s", channel.Name) | ||||
| 		} | ||||
|  | ||||
| 		return b.mc.JoinChannel(id) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @@ -118,6 +146,7 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) { | ||||
| 		if msg.ID == "" { | ||||
| 			return "", nil | ||||
| 		} | ||||
|  | ||||
| 		return msg.ID, b.mc.DeleteMessage(msg.ID) | ||||
| 	} | ||||
|  | ||||
| @@ -129,17 +158,19 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) { | ||||
|  | ||||
| 	// 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) | ||||
| 		post, _, err := b.mc.Client.GetPost(context.TODO(), msg.ParentID, "") | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err) | ||||
| 		} | ||||
| 		if post != nil && post.RootId != "" { | ||||
| 			msg.ParentID = post.RootId | ||||
| 		} | ||||
| 		msg.ParentID = post.RootId | ||||
| 	} | ||||
|  | ||||
| 	// Upload a file if it exists | ||||
| 	if msg.Extra != nil { | ||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | ||||
| 			if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil { | ||||
| 			if _, err := b.mc.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil { | ||||
| 				b.Log.Errorf("PostMessage failed: %s", err) | ||||
| 			} | ||||
| 		} | ||||
| @@ -159,5 +190,5 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) { | ||||
| 	} | ||||
|  | ||||
| 	// Post normal message | ||||
| 	return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID) | ||||
| 	return b.mc.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID) | ||||
| } | ||||
|   | ||||
| @@ -19,8 +19,10 @@ import ( | ||||
| 	"golang.org/x/oauth2" | ||||
| ) | ||||
|  | ||||
| var defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"} | ||||
| var attachRE = regexp.MustCompile(`<attachment id=.*?attachment>`) | ||||
| var ( | ||||
| 	defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"} | ||||
| 	attachRE      = regexp.MustCompile(`<attachment id=.*?attachment>`) | ||||
| ) | ||||
|  | ||||
| type Bmsteams struct { | ||||
| 	gc    *msgraph.GraphServiceRequestBuilder | ||||
| @@ -50,7 +52,7 @@ func (b *Bmsteams) Connect() error { | ||||
| 		b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err) | ||||
| 	} | ||||
| 	// make file readable only for matterbridge user | ||||
| 	err = os.Chmod(tokenCachePath, 0600) | ||||
| 	err = os.Chmod(tokenCachePath, 0o600) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err) | ||||
| 	} | ||||
| @@ -168,7 +170,7 @@ func (b *Bmsteams) poll(channelName string) error { | ||||
| 			} | ||||
|  | ||||
| 			// skip non-user message for now. | ||||
| 			if msg.From.User == nil { | ||||
| 			if msg.From == nil || msg.From.User == nil { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
|   | ||||
							
								
								
									
										70
									
								
								bridge/mumble/codec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,70 @@ | ||||
| package bmumble | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"layeh.com/gumble/gumble" | ||||
| ) | ||||
|  | ||||
| // This is a dummy implementation of a Gumble audio codec which claims | ||||
| // to implement Opus, but does not actually do anything.  This serves | ||||
| // as a workaround until https://github.com/layeh/gumble/pull/61 is | ||||
| // merged. | ||||
| // See https://github.com/42wim/matterbridge/issues/1750 for details. | ||||
|  | ||||
| const ( | ||||
| 	audioCodecIDOpus = 4 | ||||
| ) | ||||
|  | ||||
| func registerNullCodecAsOpus() { | ||||
| 	codec := &NullCodec{ | ||||
| 		encoder: &NullAudioEncoder{}, | ||||
| 		decoder: &NullAudioDecoder{}, | ||||
| 	} | ||||
| 	gumble.RegisterAudioCodec(audioCodecIDOpus, codec) | ||||
| } | ||||
|  | ||||
| type NullCodec struct { | ||||
| 	encoder *NullAudioEncoder | ||||
| 	decoder *NullAudioDecoder | ||||
| } | ||||
|  | ||||
| func (c *NullCodec) ID() int { | ||||
| 	return audioCodecIDOpus | ||||
| } | ||||
|  | ||||
| func (c *NullCodec) NewEncoder() gumble.AudioEncoder { | ||||
| 	e := &NullAudioEncoder{} | ||||
| 	return e | ||||
| } | ||||
|  | ||||
| func (c *NullCodec) NewDecoder() gumble.AudioDecoder { | ||||
| 	d := &NullAudioDecoder{} | ||||
| 	return d | ||||
| } | ||||
|  | ||||
| type NullAudioEncoder struct{} | ||||
|  | ||||
| func (e *NullAudioEncoder) ID() int { | ||||
| 	return audioCodecIDOpus | ||||
| } | ||||
|  | ||||
| func (e *NullAudioEncoder) Encode(pcm []int16, mframeSize, maxDataBytes int) ([]byte, error) { | ||||
| 	return nil, fmt.Errorf("not implemented") | ||||
| } | ||||
|  | ||||
| func (e *NullAudioEncoder) Reset() { | ||||
| } | ||||
|  | ||||
| type NullAudioDecoder struct{} | ||||
|  | ||||
| func (d *NullAudioDecoder) ID() int { | ||||
| 	return audioCodecIDOpus | ||||
| } | ||||
|  | ||||
| func (d *NullAudioDecoder) Decode(data []byte, frameSize int) ([]int16, error) { | ||||
| 	return nil, fmt.Errorf("not implemented") | ||||
| } | ||||
|  | ||||
| func (d *NullAudioDecoder) Reset() { | ||||
| } | ||||
| @@ -42,7 +42,14 @@ func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) { | ||||
| 		if part.Image == nil { | ||||
| 			rmsg.Text = part.Text | ||||
| 		} else { | ||||
| 			fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + part.FileExtension | ||||
| 			fileExt := part.FileExtension | ||||
| 			if fileExt == ".jfif" { | ||||
| 				fileExt = ".jpg" | ||||
| 			} | ||||
| 			if fileExt == ".jpe" { | ||||
| 				fileExt = ".jpg" | ||||
| 			} | ||||
| 			fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + fileExt | ||||
| 			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") | ||||
| @@ -62,7 +69,6 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) { | ||||
| 	} | ||||
| 	// 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 { | ||||
| @@ -78,19 +84,75 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) { | ||||
| 	// Only care about changes to self | ||||
| 	if event.User != event.Client.Self { | ||||
| func (b *Bmumble) handleJoinLeave(event *gumble.UserChangeEvent) { | ||||
| 	// Ignore events happening before setup is done | ||||
| 	if b.Channel == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	// Someone attempted to move the user out of the configured channel; attempt to join back | ||||
| 	if b.Channel != nil { | ||||
| 	if b.GetBool("nosendjoinpart") { | ||||
| 		return | ||||
| 	} | ||||
| 	b.Log.Debugf("Received gumble user change event: %+v", event) | ||||
|  | ||||
| 	text := "" | ||||
| 	switch { | ||||
| 	case event.Type&gumble.UserChangeKicked > 0: | ||||
| 		text = " was kicked" | ||||
| 	case event.Type&gumble.UserChangeBanned > 0: | ||||
| 		text = " was banned" | ||||
| 	case event.Type&gumble.UserChangeDisconnected > 0: | ||||
| 		if event.User.Channel != nil && event.User.Channel.ID == *b.Channel { | ||||
| 			text = " left" | ||||
| 		} | ||||
| 	case event.Type&gumble.UserChangeConnected > 0: | ||||
| 		if event.User.Channel != nil && event.User.Channel.ID == *b.Channel { | ||||
| 			text = " joined" | ||||
| 		} | ||||
| 	case event.Type&gumble.UserChangeChannel > 0: | ||||
| 		// Treat Mumble channel changes the same as connects/disconnects; as far as matterbridge is concerned, they are identical | ||||
| 		if event.User.Channel != nil && event.User.Channel.ID == *b.Channel { | ||||
| 			text = " joined" | ||||
| 		} else { | ||||
| 			text = " left" | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if text != "" { | ||||
| 		b.Remote <- config.Message{ | ||||
| 			Username: "system", | ||||
| 			Text:     event.User.Name + text, | ||||
| 			Channel:  strconv.FormatUint(uint64(*b.Channel), 10), | ||||
| 			Account:  b.Account, | ||||
| 			Event:    config.EventJoinLeave, | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) handleUserModified(event *gumble.UserChangeEvent) { | ||||
| 	// Ignore events happening before setup is done | ||||
| 	if b.Channel == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if event.Type&gumble.UserChangeChannel > 0 { | ||||
| 		// Someone attempted to move the user out of the configured channel; attempt to join back | ||||
| 		if err := b.doJoin(event.Client, *b.Channel); err != nil { | ||||
| 			b.Log.Error(err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) { | ||||
| 	// The UserChangeEvent is used for both the gumble client itself as well as other clients | ||||
| 	if event.User != event.Client.Self { | ||||
| 		// other users | ||||
| 		b.handleJoinLeave(event) | ||||
| 	} else { | ||||
| 		// gumble user | ||||
| 		b.handleUserModified(event) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) { | ||||
| 	b.connected <- *event | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"layeh.com/gumble/gumble" | ||||
| @@ -92,7 +93,7 @@ func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error { | ||||
| 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 { | ||||
| 	if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| @@ -184,6 +185,7 @@ func (b *Bmumble) doConnect() error { | ||||
| 		gumbleConfig.Password = password | ||||
| 	} | ||||
|  | ||||
| 	registerNullCodecAsOpus() | ||||
| 	client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @@ -248,12 +250,19 @@ func (b *Bmumble) processMessage(msg *config.Message) { | ||||
| 	// 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)) | ||||
| 		if *maxLength != 0 { // Some servers will have unlimited message lengths. | ||||
| 			// Not doing this makes underflows happen. | ||||
| 			msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped")) | ||||
| 		} else { | ||||
| 			msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped")) | ||||
| 		} | ||||
| 	} else { | ||||
| 		msgLines = helper.GetSubLines(msg.Text, 0) | ||||
| 		msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped")) | ||||
| 	} | ||||
| 	// Send the individual lindes | ||||
| 	// Send the individual lines | ||||
| 	for i := range msgLines { | ||||
| 		// Remove unnecessary newline character, since either way we're sending it as individual lines | ||||
| 		msgLines[i] = strings.TrimSuffix(msgLines[i], "\n") | ||||
| 		b.client.Self.Channel.Send(msg.Username+msgLines[i], false) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -74,12 +74,6 @@ func (b *Btalk) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	} | ||||
| 	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 | ||||
| @@ -90,35 +84,23 @@ func (b *Btalk) JoinChannel(channel config.ChannelInfo) 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) | ||||
|  | ||||
| 			// Ignore messages that are from the bot user | ||||
| 			if msg.ActorID == b.user.User || msg.ActorType == "bridged" { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// Handle deleting messages | ||||
| 			if msg.MessageType == ocs.MessageSystem && msg.Parent != nil && msg.Parent.MessageType == ocs.MessageDelete { | ||||
| 				b.handleDeletingMessage(&msg, &newRoom) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// Handle sending messages | ||||
| 			if msg.MessageType == ocs.MessageComment { | ||||
| 				b.handleSendingMessage(&msg, &newRoom) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			b.Log.Debugf("<= Message is %#v", remoteMessage) | ||||
| 			b.Remote <- remoteMessage | ||||
| 		} | ||||
| 	}() | ||||
| 	return nil | ||||
| @@ -131,16 +113,40 @@ func (b *Btalk) Send(msg config.Message) (string, error) { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	// Talk currently only supports sending normal messages | ||||
| 	if msg.Event != "" { | ||||
| 		return "", nil | ||||
| 	// Standard Message Send | ||||
| 	if msg.Event == "" { | ||||
| 		// Handle sending files if they are included | ||||
| 		err := b.handleSendingFile(&msg, r) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("Could not send files in message to room %v from %v: %v", msg.Channel, msg.Username, err) | ||||
|  | ||||
| 			return "", nil | ||||
| 		} | ||||
|  | ||||
| 		sentMessage, err := b.sendText(r, &msg, msg.Text) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err) | ||||
|  | ||||
| 			return "", nil | ||||
| 		} | ||||
| 		return strconv.Itoa(sentMessage.ID), nil | ||||
| 	} | ||||
| 	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 | ||||
|  | ||||
| 	// Message Deletion | ||||
| 	if msg.Event == config.EventMsgDelete { | ||||
| 		messageID, err := strconv.Atoi(msg.ID) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		data, err := r.room.DeleteMessage(messageID) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		return strconv.Itoa(data.ID), nil | ||||
| 	} | ||||
| 	return strconv.Itoa(sentMessage.ID), nil | ||||
|  | ||||
| 	// Message is not a type that is currently supported | ||||
| 	return "", nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) getRoom(token string) *Broom { | ||||
| @@ -152,6 +158,17 @@ func (b *Btalk) getRoom(token string) *Broom { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) sendText(r *Broom, msg *config.Message, text string) (*ocs.TalkRoomMessageData, error) { | ||||
| 	messageToSend := &room.Message{Message: msg.Username + text} | ||||
|  | ||||
| 	if b.GetBool("SeparateDisplayName") { | ||||
| 		messageToSend.Message = text | ||||
| 		messageToSend.ActorDisplayName = msg.Username | ||||
| 	} | ||||
|  | ||||
| 	return r.room.SendComplexMessage(messageToSend) | ||||
| } | ||||
|  | ||||
| func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error { | ||||
| 	for _, parameter := range message.MessageParameters { | ||||
| 		if parameter.Type == ocs.ROSTypeFile { | ||||
| @@ -177,6 +194,74 @@ func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageDa | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) handleSendingFile(msg *config.Message, r *Broom) error { | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi := f.(config.FileInfo) | ||||
| 		if fi.URL == "" { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		message := "" | ||||
| 		if fi.Comment != "" { | ||||
| 			message += fi.Comment + " " | ||||
| 		} | ||||
| 		message += fi.URL | ||||
| 		_, err := b.sendText(r, msg, message) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) handleSendingMessage(msg *ocs.TalkRoomMessageData, r *Broom) { | ||||
| 	remoteMessage := config.Message{ | ||||
| 		Text:     formatRichObjectString(msg.Message, msg.MessageParameters), | ||||
| 		Channel:  r.room.Token, | ||||
| 		Username: DisplayName(msg, b.guestSuffix()), | ||||
| 		UserID:   msg.ActorID, | ||||
| 		Account:  b.Account, | ||||
| 	} | ||||
| 	// It is possible for the ID to not be set on older versions of Talk so we only set it if | ||||
| 	// the ID is not blank | ||||
| 	if msg.ID != 0 { | ||||
| 		remoteMessage.ID = strconv.Itoa(msg.ID) | ||||
| 	} | ||||
|  | ||||
| 	// Handle Files | ||||
| 	err := b.handleFiles(&remoteMessage, msg) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Error handling file: %#v", msg) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("<= Message is %#v", remoteMessage) | ||||
| 	b.Remote <- remoteMessage | ||||
| } | ||||
|  | ||||
| func (b *Btalk) handleDeletingMessage(msg *ocs.TalkRoomMessageData, r *Broom) { | ||||
| 	remoteMessage := config.Message{ | ||||
| 		Event:   config.EventMsgDelete, | ||||
| 		Text:    config.EventMsgDelete, | ||||
| 		Channel: r.room.Token, | ||||
| 		ID:      strconv.Itoa(msg.Parent.ID), | ||||
| 		Account: b.Account, | ||||
| 	} | ||||
| 	b.Log.Debugf("<= Message being deleted is %#v", remoteMessage) | ||||
| 	b.Remote <- remoteMessage | ||||
| } | ||||
|  | ||||
| func (b *Btalk) guestSuffix() string { | ||||
| 	guestSuffix := " (Guest)" | ||||
| 	if b.IsKeySet("GuestSuffix") { | ||||
| 		guestSuffix = b.GetString("GuestSuffix") | ||||
| 	} | ||||
|  | ||||
| 	return guestSuffix | ||||
| } | ||||
|  | ||||
| // Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785 | ||||
| func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string { | ||||
| 	for id, parameter := range parameters { | ||||
| @@ -197,7 +282,7 @@ func formatRichObjectString(message string, parameters map[string]ocs.RichObject | ||||
| 	return message | ||||
| } | ||||
|  | ||||
| func DisplayName(msg ocs.TalkRoomMessageData, suffix string) string { | ||||
| func DisplayName(msg *ocs.TalkRoomMessageData, suffix string) string { | ||||
| 	if msg.ActorType == ocs.ActorGuest { | ||||
| 		if msg.ActorDisplayName == "" { | ||||
| 			return "Guest" | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| package brocketchat | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/matterbridge/Rocket.Chat.Go.SDK/models" | ||||
| ) | ||||
|  | ||||
| @@ -58,6 +61,7 @@ func (b *Brocketchat) handleStatusEvent(ev models.Message, rmsg *config.Message) | ||||
|  | ||||
| func (b *Brocketchat) handleRocketClient(messages chan *config.Message) { | ||||
| 	for message := range b.messageChan { | ||||
| 		message := message | ||||
| 		// skip messages with same ID, apparently messages get duplicated for an unknown reason | ||||
| 		if _, ok := b.cache.Get(message.ID); ok { | ||||
| 			continue | ||||
| @@ -76,8 +80,11 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) { | ||||
| 			Account:  b.Account, | ||||
| 			UserID:   message.User.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) { | ||||
| @@ -86,6 +93,38 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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 { | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi := f.(config.FileInfo) | ||||
|   | ||||
| @@ -135,6 +135,7 @@ func (b *Brocketchat) uploadFile(fi *config.FileInfo, channel string) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
|   | ||||
| @@ -27,7 +27,8 @@ func (b *Bslack) handleSlack() { | ||||
| 	b.Log.Debug("Start listening for Slack messages") | ||||
| 	for message := range messages { | ||||
| 		// don't do any action on deleted/typing messages | ||||
| 		if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete { | ||||
| 		if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete && | ||||
| 			message.Event != config.EventFileDelete { | ||||
| 			b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) | ||||
| 			// cleanup the message | ||||
| 			message.Text = b.replaceMention(message.Text) | ||||
| @@ -76,6 +77,13 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) { | ||||
| 				continue | ||||
| 			} | ||||
| 			messages <- rmsg | ||||
| 		case *slack.FileDeletedEvent: | ||||
| 			rmsg, err := b.handleFileDeletedEvent(ev) | ||||
| 			if err != nil { | ||||
| 				b.Log.Printf("%#v", err) | ||||
| 				continue | ||||
| 			} | ||||
| 			messages <- rmsg | ||||
| 		case *slack.OutgoingErrorEvent: | ||||
| 			b.Log.Debugf("%#v", ev.Error()) | ||||
| 		case *slack.ChannelJoinedEvent: | ||||
| @@ -95,6 +103,8 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) { | ||||
| 			b.users.populateUser(ev.User) | ||||
| 		case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent: | ||||
| 			continue | ||||
| 		case *slack.UserChangeEvent: | ||||
| 			b.users.invalidateUser(ev.User.ID) | ||||
| 		default: | ||||
| 			b.Log.Debugf("Unhandled incoming event: %T", ev) | ||||
| 		} | ||||
| @@ -220,6 +230,26 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er | ||||
| 	return rmsg, nil | ||||
| } | ||||
|  | ||||
| func (b *Bslack) handleFileDeletedEvent(ev *slack.FileDeletedEvent) (*config.Message, error) { | ||||
| 	if rawChannel, ok := b.cache.Get(cfileDownloadChannel + ev.FileID); ok { | ||||
| 		channel, err := b.channels.getChannelByID(rawChannel.(string)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		return &config.Message{ | ||||
| 			Event:    config.EventFileDelete, | ||||
| 			Text:     config.EventFileDelete, | ||||
| 			Channel:  channel.Name, | ||||
| 			Account:  b.Account, | ||||
| 			ID:       ev.FileID, | ||||
| 			Protocol: b.Protocol, | ||||
| 		}, nil | ||||
| 	} | ||||
|  | ||||
| 	return nil, fmt.Errorf("channel ID for file ID %s not found", ev.FileID) | ||||
| } | ||||
|  | ||||
| func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool { | ||||
| 	switch ev.SubType { | ||||
| 	case sChannelJoined, sMemberJoined: | ||||
| @@ -252,6 +282,13 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func getMessageTitle(attach *slack.Attachment) string { | ||||
| 	if attach.TitleLink != "" { | ||||
| 		return fmt.Sprintf("[%s](%s)\n", attach.Title, attach.TitleLink) | ||||
| 	} | ||||
| 	return attach.Title | ||||
| } | ||||
|  | ||||
| func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) { | ||||
| 	// File comments are set by the system (because there is no username given). | ||||
| 	if ev.SubType == sFileComment { | ||||
| @@ -260,12 +297,15 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) | ||||
|  | ||||
| 	// See if we have some text in the attachments. | ||||
| 	if rmsg.Text == "" { | ||||
| 		for _, attach := range ev.Attachments { | ||||
| 		for i, attach := range ev.Attachments { | ||||
| 			if attach.Text != "" { | ||||
| 				if attach.Title != "" { | ||||
| 					rmsg.Text = attach.Title + "\n" | ||||
| 					rmsg.Text = getMessageTitle(&ev.Attachments[i]) | ||||
| 				} | ||||
| 				rmsg.Text += attach.Text | ||||
| 				if attach.Footer != "" { | ||||
| 					rmsg.Text += "\n\n" + attach.Footer | ||||
| 				} | ||||
| 			} else { | ||||
| 				rmsg.Text = attach.Fallback | ||||
| 			} | ||||
| @@ -279,6 +319,8 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) | ||||
|  | ||||
| 	// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra. | ||||
| 	for i := range ev.Files { | ||||
| 		// keep reference in cache on which channel we added this file | ||||
| 		b.cache.Add(cfileDownloadChannel+ev.Files[i].ID, ev.Channel) | ||||
| 		if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil { | ||||
| 			b.Log.Errorf("Could not download incoming file: %#v", err) | ||||
| 		} | ||||
| @@ -328,7 +370,7 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retr | ||||
| 	// that the comment is not duplicated. | ||||
| 	comment := rmsg.Text | ||||
| 	rmsg.Text = "" | ||||
| 	helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General) | ||||
| 	helper.HandleDownloadData2(b.Log, rmsg, file.Name, file.ID, comment, file.URLPrivateDownload, data, b.General) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -87,6 +87,9 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi | ||||
| 	if user.Profile.DisplayName != "" { | ||||
| 		rmsg.Username = user.Profile.DisplayName | ||||
| 	} | ||||
| 	if b.GetBool("UseFullName") && user.Profile.RealName != "" { | ||||
| 		rmsg.Username = user.Profile.RealName | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @@ -98,7 +101,9 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config | ||||
| 	var err error | ||||
| 	var bot *slack.Bot | ||||
| 	for { | ||||
| 		bot, err = b.rtm.GetBotInfo(ev.BotID) | ||||
| 		bot, err = b.rtm.GetBotInfo(slack.GetBotInfoParameters{ | ||||
| 			Bot: ev.BotID, | ||||
| 		}) | ||||
| 		if err == nil { | ||||
| 			break | ||||
| 		} | ||||
| @@ -124,7 +129,7 @@ var ( | ||||
| 	mentionRE        = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`) | ||||
| 	channelRE        = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`) | ||||
| 	variableRE       = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`) | ||||
| 	urlRE            = regexp.MustCompile(`<(.*?)(\|.*?)?>`) | ||||
| 	urlRE            = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) | ||||
| 	codeFenceRE      = regexp.MustCompile(`(?m)^` + "```" + `\w+$`) | ||||
| 	topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`) | ||||
| ) | ||||
| @@ -178,14 +183,7 @@ func (b *Bslack) replaceVariable(text string) string { | ||||
|  | ||||
| // @see https://api.slack.com/docs/message-formatting#linking_to_urls | ||||
| func (b *Bslack) replaceURL(text string) string { | ||||
| 	for _, r := range urlRE.FindAllStringSubmatch(text, -1) { | ||||
| 		if len(strings.TrimSpace(r[2])) == 1 { // A display text separator was found, but the text was blank | ||||
| 			text = strings.Replace(text, r[0], "", 1) | ||||
| 		} else { | ||||
| 			text = strings.Replace(text, r[0], r[1], 1) | ||||
| 		} | ||||
| 	} | ||||
| 	return text | ||||
| 	return urlRE.ReplaceAllString(text, "[${2}](${1})") | ||||
| } | ||||
|  | ||||
| func (b *Bslack) replaceb0rkedMarkDown(text string) string { | ||||
|   | ||||
| @@ -36,24 +36,25 @@ type Bslack struct { | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	sHello           = "hello" | ||||
| 	sChannelJoin     = "channel_join" | ||||
| 	sChannelLeave    = "channel_leave" | ||||
| 	sChannelJoined   = "channel_joined" | ||||
| 	sMemberJoined    = "member_joined_channel" | ||||
| 	sMessageChanged  = "message_changed" | ||||
| 	sMessageDeleted  = "message_deleted" | ||||
| 	sSlackAttachment = "slack_attachment" | ||||
| 	sPinnedItem      = "pinned_item" | ||||
| 	sUnpinnedItem    = "unpinned_item" | ||||
| 	sChannelTopic    = "channel_topic" | ||||
| 	sChannelPurpose  = "channel_purpose" | ||||
| 	sFileComment     = "file_comment" | ||||
| 	sMeMessage       = "me_message" | ||||
| 	sUserTyping      = "user_typing" | ||||
| 	sLatencyReport   = "latency_report" | ||||
| 	sSystemUser      = "system" | ||||
| 	sSlackBotUser    = "slackbot" | ||||
| 	sHello               = "hello" | ||||
| 	sChannelJoin         = "channel_join" | ||||
| 	sChannelLeave        = "channel_leave" | ||||
| 	sChannelJoined       = "channel_joined" | ||||
| 	sMemberJoined        = "member_joined_channel" | ||||
| 	sMessageChanged      = "message_changed" | ||||
| 	sMessageDeleted      = "message_deleted" | ||||
| 	sSlackAttachment     = "slack_attachment" | ||||
| 	sPinnedItem          = "pinned_item" | ||||
| 	sUnpinnedItem        = "unpinned_item" | ||||
| 	sChannelTopic        = "channel_topic" | ||||
| 	sChannelPurpose      = "channel_purpose" | ||||
| 	sFileComment         = "file_comment" | ||||
| 	sMeMessage           = "me_message" | ||||
| 	sUserTyping          = "user_typing" | ||||
| 	sLatencyReport       = "latency_report" | ||||
| 	sSystemUser          = "system" | ||||
| 	sSlackBotUser        = "slackbot" | ||||
| 	cfileDownloadChannel = "file_download_channel" | ||||
|  | ||||
| 	tokenConfig           = "Token" | ||||
| 	incomingWebhookConfig = "WebhookBindAddress" | ||||
| @@ -156,7 +157,7 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error { | ||||
|  | ||||
| 	// try to join a channel when in legacy | ||||
| 	if b.legacy { | ||||
| 		_, err := b.sc.JoinChannel(channel.Name) | ||||
| 		_, _, _, err := b.sc.JoinConversation(channel.Name) | ||||
| 		if err != nil { | ||||
| 			switch err.Error() { | ||||
| 			case "name_taken", "restricted_action": | ||||
| @@ -195,7 +196,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) { | ||||
| 		b.Log.Debugf("=> Receiving %#v", msg) | ||||
| 	} | ||||
|  | ||||
| 	msg.Text = helper.ClipMessage(msg.Text, messageLength) | ||||
| 	msg.Text = helper.ClipMessage(msg.Text, messageLength, b.GetString("MessageClipped")) | ||||
| 	msg.Text = b.replaceCodeFence(msg.Text) | ||||
|  | ||||
| 	// Make a action /me of the message | ||||
| @@ -320,7 +321,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) { | ||||
| 	} | ||||
|  | ||||
| 	// Upload a file if it exists. | ||||
| 	if msg.Extra != nil { | ||||
| 	if len(msg.Extra) > 0 { | ||||
| 		extraMsgs := helper.HandleExtra(&msg, b.General) | ||||
| 		for i := range extraMsgs { | ||||
| 			rmsg := &extraMsgs[i] | ||||
| @@ -331,7 +332,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) { | ||||
| 			} | ||||
| 		} | ||||
| 		// Upload files if necessary (from Slack, Telegram or Mattermost). | ||||
| 		b.uploadFile(&msg, channelInfo.ID) | ||||
| 		return b.uploadFile(&msg, channelInfo.ID) | ||||
| 	} | ||||
|  | ||||
| 	// Post message. | ||||
| @@ -442,7 +443,8 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s | ||||
| } | ||||
|  | ||||
| // uploadFile handles native upload of files | ||||
| func (b *Bslack) uploadFile(msg *config.Message, channelID string) { | ||||
| func (b *Bslack) uploadFile(msg *config.Message, channelID string) (string, error) { | ||||
| 	var messageID string | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi, ok := f.(config.FileInfo) | ||||
| 		if !ok { | ||||
| @@ -459,7 +461,7 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) { | ||||
| 		b.cache.Add("filename"+fi.Name, ts) | ||||
| 		initialComment := fmt.Sprintf("File from %s", msg.Username) | ||||
| 		if fi.Comment != "" { | ||||
| 			initialComment += fmt.Sprintf("with comment: %s", fi.Comment) | ||||
| 			initialComment += fmt.Sprintf(" with comment: %s", fi.Comment) | ||||
| 		} | ||||
| 		res, err := b.sc.UploadFile(slack.FileUploadParameters{ | ||||
| 			Reader:          bytes.NewReader(*fi.Data), | ||||
| @@ -470,13 +472,22 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) { | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("uploadfile %#v", err) | ||||
| 			return | ||||
| 			return "", err | ||||
| 		} | ||||
| 		if res.ID != "" { | ||||
| 			b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String()) | ||||
| 			b.cache.Add("file"+res.ID, ts) | ||||
|  | ||||
| 			// search for message id by uploaded file in private/public channels, get thread timestamp from uploaded file | ||||
| 			if v, ok := res.Shares.Private[channelID]; ok && len(v) > 0 { | ||||
| 				messageID = v[0].Ts | ||||
| 			} | ||||
| 			if v, ok := res.Shares.Public[channelID]; ok && len(v) > 0 { | ||||
| 				messageID = v[0].Ts | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return messageID, nil | ||||
| } | ||||
|  | ||||
| func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption { | ||||
|   | ||||
| @@ -113,6 +113,12 @@ func (b *users) populateUser(userID string) { | ||||
| 	b.users[userID] = user | ||||
| } | ||||
|  | ||||
| func (b *users) invalidateUser(userID string) { | ||||
| 	b.usersMutex.Lock() | ||||
| 	defer b.usersMutex.Unlock() | ||||
| 	delete(b.users, userID) | ||||
| } | ||||
|  | ||||
| func (b *users) populateUsers(wait bool) { | ||||
| 	b.refreshMutex.Lock() | ||||
| 	if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) { | ||||
| @@ -283,8 +289,9 @@ func (b *channels) populateChannels(wait bool) { | ||||
| 	// We only retrieve public and private channels, not IMs | ||||
| 	// and MPIMs as those do not have a channel name. | ||||
| 	queryParams := &slack.GetConversationsParameters{ | ||||
| 		ExcludeArchived: "true", | ||||
| 		ExcludeArchived: true, | ||||
| 		Types:           []string{"public_channel,private_channel"}, | ||||
| 		Limit:           1000, | ||||
| 	} | ||||
| 	for { | ||||
| 		channels, nextCursor, err := b.sc.GetConversations(queryParams) | ||||
|   | ||||
| @@ -1,22 +1,41 @@ | ||||
| package btelegram | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"html" | ||||
| 	"regexp" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"unicode/utf16" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" | ||||
| 	"github.com/davecgh/go-spew/spew" | ||||
| 	tgbotapi "github.com/matterbridge/telegram-bot-api/v6" | ||||
| ) | ||||
|  | ||||
| func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message { | ||||
| 	// handle channels | ||||
| 	if posted != nil { | ||||
| 		message = posted | ||||
| 		rmsg.Text = message.Text | ||||
| 		if posted.Text == "/chatId" { | ||||
| 			chatID := strconv.FormatInt(posted.Chat.ID, 10) | ||||
|  | ||||
| 			// Handle chat topics | ||||
| 			if posted.IsTopicMessage { | ||||
| 				chatID = chatID + "/" + strconv.Itoa(posted.MessageThreadID) | ||||
| 			} | ||||
|  | ||||
| 			_, err := b.Send(config.Message{ | ||||
| 				Channel: chatID, | ||||
| 				Text:    fmt.Sprintf("ID of this chat: %s", chatID), | ||||
| 			}) | ||||
| 			if err != nil { | ||||
| 				b.Log.Warnf("Unable to send chatID to %s", chatID) | ||||
| 			} | ||||
| 		} else { | ||||
| 			message = posted | ||||
| 			rmsg.Text = message.Text | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// edited channel message | ||||
| @@ -43,6 +62,11 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if message.ForwardFromChat != nil && message.ForwardFrom == nil { | ||||
| 		rmsg.Text = "Forwarded from " + message.ForwardFromChat.Title + ": " + rmsg.Text | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if message.ForwardFrom == nil { | ||||
| 		rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text | ||||
| 		return | ||||
| @@ -52,6 +76,9 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess | ||||
| 	if b.GetBool("UseFirstName") { | ||||
| 		usernameForward = message.ForwardFrom.FirstName | ||||
| 	} | ||||
| 	if b.GetBool("UseFullName") { | ||||
| 		usernameForward = message.ForwardFrom.FirstName + " " + message.ForwardFrom.LastName | ||||
| 	} | ||||
|  | ||||
| 	if usernameForward == "" { | ||||
| 		usernameForward = message.ForwardFrom.UserName | ||||
| @@ -69,12 +96,16 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess | ||||
|  | ||||
| // handleQuoting handles quoting of previous messages | ||||
| func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) { | ||||
| 	if message.ReplyToMessage != nil { | ||||
| 	// Used to check if the message was a reply to the root topic | ||||
| 	if message.ReplyToMessage != nil && (!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) { //nolint:nestif | ||||
| 		usernameReply := "" | ||||
| 		if message.ReplyToMessage.From != nil { | ||||
| 			if b.GetBool("UseFirstName") { | ||||
| 				usernameReply = message.ReplyToMessage.From.FirstName | ||||
| 			} | ||||
| 			if b.GetBool("UseFullName") { | ||||
| 				usernameReply = message.ReplyToMessage.From.FirstName + " " + message.ReplyToMessage.From.LastName | ||||
| 			} | ||||
| 			if usernameReply == "" { | ||||
| 				usernameReply = message.ReplyToMessage.From.UserName | ||||
| 				if usernameReply == "" { | ||||
| @@ -86,7 +117,11 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag | ||||
| 			usernameReply = unknownUser | ||||
| 		} | ||||
| 		if !b.GetBool("QuoteDisable") { | ||||
| 			rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text) | ||||
| 			quote := message.ReplyToMessage.Text | ||||
| 			if quote == "" { | ||||
| 				quote = message.ReplyToMessage.Caption | ||||
| 			} | ||||
| 			rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, quote) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -94,10 +129,15 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag | ||||
| // handleUsername handles the correct setting of the username | ||||
| func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) { | ||||
| 	if message.From != nil { | ||||
| 		rmsg.UserID = strconv.Itoa(message.From.ID) | ||||
| 		rmsg.UserID = strconv.FormatInt(message.From.ID, 10) | ||||
| 		if b.GetBool("UseFirstName") { | ||||
| 			rmsg.Username = message.From.FirstName | ||||
| 		} | ||||
| 		if b.GetBool("UseFullName") { | ||||
| 			if message.From.FirstName != "" && message.From.LastName != "" { | ||||
| 				rmsg.Username = message.From.FirstName + " " + message.From.LastName | ||||
| 			} | ||||
| 		} | ||||
| 		if rmsg.Username == "" { | ||||
| 			rmsg.Username = message.From.UserName | ||||
| 			if rmsg.Username == "" { | ||||
| @@ -110,6 +150,35 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if message.SenderChat != nil { //nolint:nestif | ||||
| 		rmsg.UserID = strconv.FormatInt(message.SenderChat.ID, 10) | ||||
| 		if b.GetBool("UseFirstName") { | ||||
| 			rmsg.Username = message.SenderChat.FirstName | ||||
| 		} | ||||
| 		if b.GetBool("UseFullName") { | ||||
| 			if message.SenderChat.FirstName != "" && message.SenderChat.LastName != "" { | ||||
| 				rmsg.Username = message.SenderChat.FirstName + " " + message.SenderChat.LastName | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if rmsg.Username == "" || rmsg.Username == "Channel_Bot" { | ||||
| 			rmsg.Username = message.SenderChat.UserName | ||||
|  | ||||
| 			if rmsg.Username == "" || rmsg.Username == "Channel_Bot" { | ||||
| 				rmsg.Username = message.SenderChat.FirstName | ||||
| 			} | ||||
| 		} | ||||
| 		// only download avatars if we have a place to upload them (configured mediaserver) | ||||
| 		if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") { | ||||
| 			b.handleDownloadAvatar(message.SenderChat.ID, rmsg.Channel) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Fallback on author signature (used in "channel" type of chat) | ||||
| 	if rmsg.Username == "" && message.AuthorSignature != "" { | ||||
| 		rmsg.Username = message.AuthorSignature | ||||
| 	} | ||||
|  | ||||
| 	// if we really didn't find a username, set it to unknown | ||||
| 	if rmsg.Username == "" { | ||||
| 		rmsg.Username = unknownUser | ||||
| @@ -122,10 +191,16 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { | ||||
|  | ||||
| 		if update.Message == nil && update.ChannelPost == nil && | ||||
| 			update.EditedMessage == nil && update.EditedChannelPost == nil { | ||||
| 			b.Log.Error("Getting nil messages, this shouldn't happen.") | ||||
| 			b.Log.Info("Received event without messages, skipping.") | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if b.GetInt("debuglevel") == 1 { | ||||
| 			spew.Dump(update.Message) | ||||
| 		} | ||||
|  | ||||
| 		b.handleGroupUpdate(update) | ||||
|  | ||||
| 		var message *tgbotapi.Message | ||||
|  | ||||
| 		rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})} | ||||
| @@ -144,6 +219,19 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { | ||||
| 		// set the ID's from the channel or group message | ||||
| 		rmsg.ID = strconv.Itoa(message.MessageID) | ||||
| 		rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10) | ||||
| 		if message.IsTopicMessage { | ||||
| 			rmsg.Channel += "/" + strconv.Itoa(message.MessageThreadID) | ||||
| 		} | ||||
|  | ||||
| 		// preserve threading from telegram reply | ||||
| 		if message.ReplyToMessage != nil && | ||||
| 			// Used to check if the message was a reply to the root topic | ||||
| 			(!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) { | ||||
| 			rmsg.ParentID = strconv.Itoa(message.ReplyToMessage.MessageID) | ||||
| 		} | ||||
|  | ||||
| 		// handle entities (adding URLs) | ||||
| 		b.handleEntities(&rmsg, message) | ||||
|  | ||||
| 		// handle username | ||||
| 		b.handleUsername(&rmsg, message) | ||||
| @@ -160,14 +248,12 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { | ||||
| 		// quote the previous message | ||||
| 		b.handleQuoting(&rmsg, message) | ||||
|  | ||||
| 		// handle entities (adding URLs) | ||||
| 		b.handleEntities(&rmsg, message) | ||||
|  | ||||
| 		if rmsg.Text != "" || len(rmsg.Extra) > 0 { | ||||
| 			rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text) | ||||
| 			// Comment the next line out due to avoid removing empty lines in Telegram | ||||
| 			// rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text) | ||||
| 			// channels don't have (always?) user information. see #410 | ||||
| 			if message.From != nil { | ||||
| 				rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General) | ||||
| 				rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.FormatInt(message.From.ID, 10), b.General) | ||||
| 			} | ||||
|  | ||||
| 			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) | ||||
| @@ -177,61 +263,99 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // handleDownloadAvatar downloads the avatar of userid from channel | ||||
| // sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. | ||||
| // logs an error message if it fails | ||||
| func (b *Btelegram) handleDownloadAvatar(userid int, channel string) { | ||||
| 	rmsg := config.Message{Username: "system", | ||||
| 		Text:    "avatar", | ||||
| 		Channel: channel, | ||||
| 		Account: b.Account, | ||||
| 		UserID:  strconv.Itoa(userid), | ||||
| 		Event:   config.EventAvatarDownload, | ||||
| 		Extra:   make(map[string][]interface{})} | ||||
|  | ||||
| 	if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok { | ||||
| 		photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1}) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("Userprofile download failed for %#v %s", userid, err) | ||||
| 		} | ||||
|  | ||||
| 		if len(photos.Photos) > 0 { | ||||
| 			photo := photos.Photos[0][0] | ||||
| 			url := b.getFileDirectURL(photo.FileID) | ||||
| 			name := strconv.Itoa(userid) + ".png" | ||||
| 			b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize) | ||||
|  | ||||
| 			err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General) | ||||
| 			if err != nil { | ||||
| 				b.Log.Error(err) | ||||
| 				return | ||||
| 			} | ||||
| 			data, err := helper.DownloadFile(url) | ||||
| 			if err != nil { | ||||
| 				b.Log.Errorf("download %s failed %#v", url, err) | ||||
| 				return | ||||
| 			} | ||||
| 			helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General) | ||||
| 			b.Remote <- rmsg | ||||
| func (b *Btelegram) handleGroupUpdate(update tgbotapi.Update) { | ||||
| 	if msg := update.Message; msg != nil { | ||||
| 		switch { | ||||
| 		case msg.NewChatMembers != nil: | ||||
| 			b.handleUserJoin(update) | ||||
| 		case msg.LeftChatMember != nil: | ||||
| 			b.handleUserLeave(update) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Btelegram) handleUserJoin(update tgbotapi.Update) { | ||||
| 	msg := update.Message | ||||
| 	for _, user := range msg.NewChatMembers { | ||||
| 		rmsg := config.Message{ | ||||
| 			UserID:   strconv.FormatInt(user.ID, 10), | ||||
| 			Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me) | ||||
| 			Channel:  strconv.FormatInt(msg.Chat.ID, 10), | ||||
| 			Account:  b.Account, | ||||
| 			Protocol: b.Protocol, | ||||
| 			Event:    config.EventJoinLeave, | ||||
| 			Text:     "joined chat", | ||||
| 		} | ||||
| 		b.Remote <- rmsg | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Btelegram) handleUserLeave(update tgbotapi.Update) { | ||||
| 	msg := update.Message | ||||
| 	user := msg.LeftChatMember | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   strconv.FormatInt(user.ID, 10), | ||||
| 		Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me) | ||||
| 		Channel:  strconv.FormatInt(msg.Chat.ID, 10), | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Event:    config.EventJoinLeave, | ||||
| 		Text:     "left chat", | ||||
| 	} | ||||
|  | ||||
| 	b.Remote <- rmsg | ||||
| } | ||||
|  | ||||
| // handleDownloadAvatar downloads the avatar of userid from channel | ||||
| // sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. | ||||
| // logs an error message if it fails | ||||
| func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) { | ||||
| 	rmsg := config.Message{ | ||||
| 		Username: "system", | ||||
| 		Text:     "avatar", | ||||
| 		Channel:  channel, | ||||
| 		Account:  b.Account, | ||||
| 		UserID:   strconv.FormatInt(userid, 10), | ||||
| 		Event:    config.EventAvatarDownload, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 	} | ||||
|  | ||||
| 	if _, ok := b.avatarMap[strconv.FormatInt(userid, 10)]; ok { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1}) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Userprofile download failed for %#v %s", userid, err) | ||||
| 	} | ||||
|  | ||||
| 	if len(photos.Photos) > 0 { | ||||
| 		photo := photos.Photos[0][0] | ||||
| 		url := b.getFileDirectURL(photo.FileID) | ||||
| 		name := strconv.FormatInt(userid, 10) + ".png" | ||||
| 		b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize) | ||||
|  | ||||
| 		err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General) | ||||
| 		if err != nil { | ||||
| 			b.Log.Error(err) | ||||
| 			return | ||||
| 		} | ||||
| 		data, err := helper.DownloadFile(url) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("download %s failed %#v", url, err) | ||||
| 			return | ||||
| 		} | ||||
| 		helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General) | ||||
| 		b.Remote <- rmsg | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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: | ||||
| 	format := b.GetString("MediaConvertTgs") | ||||
| 	if helper.SupportsFormat(format) { | ||||
| 		b.Log.Debugf("Format supported by %s, converting %v", helper.LottieBackend(), name) | ||||
| 	} else { | ||||
| 		// Otherwise, no conversion was requested. Trying to run the usual webp | ||||
| 		// converter would fail, because '.tgs.webp' is actually a gzipped JSON | ||||
| 		// file, and has nothing to do with WebP. | ||||
| @@ -259,12 +383,12 @@ func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) { | ||||
|  | ||||
| // handleDownloadFile handles file download | ||||
| func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error { | ||||
| 	size := 0 | ||||
| 	size := int64(0) | ||||
| 	var url, name, text string | ||||
| 	switch { | ||||
| 	case message.Sticker != nil: | ||||
| 		text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true) | ||||
| 		size = message.Sticker.FileSize | ||||
| 		size = int64(message.Sticker.FileSize) | ||||
| 	case message.Voice != nil: | ||||
| 		text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true) | ||||
| 		size = message.Voice.FileSize | ||||
| @@ -280,8 +404,8 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa | ||||
| 		name = message.Document.FileName | ||||
| 		text = " " + message.Document.FileName + " : " + url | ||||
| 	case message.Photo != nil: | ||||
| 		photos := *message.Photo | ||||
| 		size = photos[len(photos)-1].FileSize | ||||
| 		photos := message.Photo | ||||
| 		size = int64(photos[len(photos)-1].FileSize) | ||||
| 		text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true) | ||||
| 	} | ||||
|  | ||||
| @@ -327,7 +451,7 @@ func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (str | ||||
| 		urlPart := strings.Split(url, "/") | ||||
| 		name = urlPart[len(urlPart)-1] | ||||
| 	} | ||||
| 	if suffix != "" && !strings.HasSuffix(name, suffix) { | ||||
| 	if suffix != "" && !strings.HasSuffix(name, suffix) && !strings.HasSuffix(name, ".webm") { | ||||
| 		name += suffix | ||||
| 	} | ||||
| 	text := " " + url | ||||
| @@ -339,11 +463,15 @@ func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, err | ||||
| 	if msg.ID == "" { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	msgid, err := strconv.Atoi(msg.ID) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid}) | ||||
|  | ||||
| 	cfg := tgbotapi.NewDeleteMessage(chatid, msgid) | ||||
| 	_, err = b.c.Request(cfg) | ||||
|  | ||||
| 	return "", err | ||||
| } | ||||
|  | ||||
| @@ -381,31 +509,57 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error | ||||
| } | ||||
|  | ||||
| // handleUploadFile handles native upload of files | ||||
| func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string { | ||||
| 	var c tgbotapi.Chattable | ||||
| func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64, threadid int, parentID int) (string, error) { | ||||
| 	var media []interface{} | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi := f.(config.FileInfo) | ||||
| 		file := tgbotapi.FileBytes{ | ||||
| 			Name:  fi.Name, | ||||
| 			Bytes: *fi.Data, | ||||
| 		} | ||||
| 		re := regexp.MustCompile(".(jpg|jpe|png)$") | ||||
| 		if re.MatchString(fi.Name) { | ||||
| 			c = tgbotapi.NewPhotoUpload(chatid, file) | ||||
| 		} else { | ||||
| 			c = tgbotapi.NewDocumentUpload(chatid, file) | ||||
|  | ||||
| 		if b.GetString("MessageFormat") == HTMLFormat { | ||||
| 			fi.Comment = makeHTML(html.EscapeString(fi.Comment)) | ||||
| 		} | ||||
| 		_, err := b.c.Send(c) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("file upload failed: %#v", err) | ||||
| 		} | ||||
| 		if fi.Comment != "" { | ||||
| 			if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil { | ||||
| 				b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err) | ||||
|  | ||||
| 		switch filepath.Ext(fi.Name) { | ||||
| 		case ".jpg", ".jpe", ".png": | ||||
| 			pc := tgbotapi.NewInputMediaPhoto(file) | ||||
| 			if fi.Comment != "" { | ||||
| 				pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) | ||||
| 			} | ||||
| 			media = append(media, pc) | ||||
| 		case ".mp4", ".m4v": | ||||
| 			vc := tgbotapi.NewInputMediaVideo(file) | ||||
| 			if fi.Comment != "" { | ||||
| 				vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) | ||||
| 			} | ||||
| 			media = append(media, vc) | ||||
| 		case ".mp3", ".oga": | ||||
| 			ac := tgbotapi.NewInputMediaAudio(file) | ||||
| 			if fi.Comment != "" { | ||||
| 				ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) | ||||
| 			} | ||||
| 			media = append(media, ac) | ||||
| 		case ".ogg": | ||||
| 			voc := tgbotapi.NewVoice(chatid, file) | ||||
| 			voc.Caption, voc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) | ||||
| 			voc.ReplyToMessageID = parentID | ||||
| 			res, err := b.c.Send(voc) | ||||
| 			if err != nil { | ||||
| 				return "", err | ||||
| 			} | ||||
| 			return strconv.Itoa(res.MessageID), nil | ||||
| 		default: | ||||
| 			dc := tgbotapi.NewInputMediaDocument(file) | ||||
| 			if fi.Comment != "" { | ||||
| 				dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) | ||||
| 			} | ||||
| 			media = append(media, dc) | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
|  | ||||
| 	return b.sendMediaFiles(msg, chatid, threadid, parentID, media) | ||||
| } | ||||
|  | ||||
| func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string { | ||||
| @@ -413,7 +567,7 @@ func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string | ||||
| 	if format == "" { | ||||
| 		format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" | ||||
| 	} | ||||
| 	quoteMessagelength := len(quoteMessage) | ||||
| 	quoteMessagelength := len([]rune(quoteMessage)) | ||||
| 	if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") { | ||||
| 		runes := []rune(quoteMessage) | ||||
| 		quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")]) | ||||
| @@ -432,21 +586,61 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa | ||||
| 	if message.Entities == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	// for now only do URL replacements | ||||
| 	for _, e := range *message.Entities { | ||||
|  | ||||
| 	indexMovedBy := 0 | ||||
| 	prevLinkOffset := -1 | ||||
|  | ||||
| 	for _, e := range message.Entities { | ||||
|  | ||||
| 		asRunes := utf16.Encode([]rune(rmsg.Text)) | ||||
|  | ||||
| 		if e.Type == "text_link" { | ||||
| 			offset := e.Offset + indexMovedBy | ||||
| 			url, err := e.ParseURL() | ||||
| 			if err != nil { | ||||
| 				b.Log.Errorf("entity text_link url parse failed: %s", err) | ||||
| 				continue | ||||
| 			} | ||||
| 			utfEncodedString := utf16.Encode([]rune(rmsg.Text)) | ||||
| 			if e.Offset+e.Length > len(utfEncodedString) { | ||||
| 				b.Log.Errorf("entity length is too long %d > %d", e.Offset+e.Length, len(utfEncodedString)) | ||||
| 			if offset+e.Length > len(utfEncodedString) { | ||||
| 				b.Log.Errorf("entity length is too long %d > %d", 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) | ||||
| 			rmsg.Text = string(utf16.Decode(asRunes[:offset+e.Length])) + " (" + url.String() + ")" + string(utf16.Decode(asRunes[offset+e.Length:])) | ||||
| 			indexMovedBy += len(url.String()) + 3 | ||||
| 			prevLinkOffset = e.Offset | ||||
| 		} | ||||
|  | ||||
| 		if e.Offset == prevLinkOffset { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if e.Type == "code" { | ||||
| 			offset := e.Offset + indexMovedBy | ||||
| 			rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "`" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "`" + string(utf16.Decode(asRunes[offset+e.Length:])) | ||||
| 			indexMovedBy += 2 | ||||
| 		} | ||||
|  | ||||
| 		if e.Type == "pre" { | ||||
| 			offset := e.Offset + indexMovedBy | ||||
| 			rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "```\n" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "```\n" + string(utf16.Decode(asRunes[offset+e.Length:])) | ||||
| 			indexMovedBy += 8 | ||||
| 		} | ||||
|  | ||||
| 		if e.Type == "bold" { | ||||
| 			offset := e.Offset + indexMovedBy | ||||
| 			rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "*" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "*" + string(utf16.Decode(asRunes[offset+e.Length:])) | ||||
| 			indexMovedBy += 2 | ||||
| 		} | ||||
| 		if e.Type == "italic" { | ||||
| 			offset := e.Offset + indexMovedBy | ||||
| 			rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "_" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "_" + string(utf16.Decode(asRunes[offset+e.Length:])) | ||||
| 			indexMovedBy += 2 | ||||
| 		} | ||||
| 		if e.Type == "strike" { | ||||
| 			offset := e.Offset + indexMovedBy | ||||
| 			rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "~" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "~" + string(utf16.Decode(asRunes[offset+e.Length:])) | ||||
| 			indexMovedBy += 2 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package btelegram | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"html" | ||||
|  | ||||
| 	"github.com/russross/blackfriday" | ||||
| ) | ||||
| @@ -24,10 +23,16 @@ func (options *customHTML) Paragraph(out *bytes.Buffer, text func() bool) { | ||||
| func (options *customHTML) BlockCode(out *bytes.Buffer, text []byte, lang string) { | ||||
| 	out.WriteString("<pre>") | ||||
|  | ||||
| 	out.WriteString(html.EscapeString(string(text))) | ||||
| 	out.WriteString(string(text)) | ||||
| 	out.WriteString("</pre>\n") | ||||
| } | ||||
|  | ||||
| func (options *customHTML) CodeSpan(out *bytes.Buffer, text []byte) { | ||||
| 	out.WriteString("<code>") | ||||
| 	out.WriteString(string(text)) | ||||
| 	out.WriteString("</code>") | ||||
| } | ||||
|  | ||||
| func (options *customHTML) Header(out *bytes.Buffer, text func() bool, level int, id string) { | ||||
| 	options.Paragraph(out, text) | ||||
| } | ||||
| @@ -42,6 +47,10 @@ func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) { | ||||
| 	out.WriteByte('\n') | ||||
| } | ||||
|  | ||||
| func (options *customHTML) LineBreak(out *bytes.Buffer) { | ||||
| 	out.WriteByte('\n') | ||||
| } | ||||
|  | ||||
| func (options *customHTML) List(out *bytes.Buffer, text func() bool, flags int) { | ||||
| 	options.Paragraph(out, text) | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package btelegram | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"html" | ||||
| 	"log" | ||||
| 	"strconv" | ||||
| @@ -9,7 +10,7 @@ import ( | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" | ||||
| 	tgbotapi "github.com/matterbridge/telegram-bot-api/v6" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -17,8 +18,6 @@ const ( | ||||
| 	HTMLFormat  = "HTML" | ||||
| 	HTMLNick    = "htmlnick" | ||||
| 	MarkdownV2  = "MarkdownV2" | ||||
| 	FormatPng   = "png" | ||||
| 	FormatWebp  = "webp" | ||||
| ) | ||||
|  | ||||
| type Btelegram struct { | ||||
| @@ -32,10 +31,10 @@ func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	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) | ||||
| 			log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s does not appear to work:\n%#v", tgsConvertFormat, helper.LottieBackend(), err) | ||||
| 		} | ||||
| 		if 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) | ||||
| 		if !helper.SupportsFormat(tgsConvertFormat) { | ||||
| 			log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s doesn't support it.", tgsConvertFormat, helper.LottieBackend()) | ||||
| 		} | ||||
| 	} | ||||
| 	return &Btelegram{Config: cfg, avatarMap: make(map[string]string)} | ||||
| @@ -51,11 +50,7 @@ func (b *Btelegram) Connect() error { | ||||
| 	} | ||||
| 	u := tgbotapi.NewUpdate(0) | ||||
| 	u.Timeout = 60 | ||||
| 	updates, err := b.c.GetUpdatesChan(u) | ||||
| 	if err != nil { | ||||
| 		b.Log.Debugf("%#v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 	updates := b.c.GetUpdatesChan(u) | ||||
| 	b.Log.Info("Connection succeeded") | ||||
| 	go b.handleRecv(updates) | ||||
| 	return nil | ||||
| @@ -69,11 +64,63 @@ func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	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) getIds(channel string) (int64, int, error) { | ||||
| 	var chatid int64 | ||||
| 	topicid := 0 | ||||
|  | ||||
| 	// get the chatid | ||||
| 	if strings.Contains(channel, "/") { //nolint:nestif | ||||
| 		s := strings.Split(channel, "/") | ||||
| 		if len(s) < 2 { | ||||
| 			b.Log.Errorf("Invalid channel format: %#v\n", channel) | ||||
| 			return 0, 0, nil | ||||
| 		} | ||||
| 		id, err := strconv.ParseInt(s[0], 10, 64) | ||||
| 		if err != nil { | ||||
| 			return 0, 0, err | ||||
| 		} | ||||
| 		chatid = id | ||||
| 		tid, err := strconv.Atoi(s[1]) | ||||
| 		if err != nil { | ||||
| 			return 0, 0, err | ||||
| 		} | ||||
| 		topicid = tid | ||||
| 	} else { | ||||
| 		id, err := strconv.ParseInt(channel, 10, 64) | ||||
| 		if err != nil { | ||||
| 			return 0, 0, err | ||||
| 		} | ||||
| 		chatid = id | ||||
| 	} | ||||
| 	return chatid, topicid, nil | ||||
| } | ||||
|  | ||||
| func (b *Btelegram) Send(msg config.Message) (string, error) { | ||||
| 	b.Log.Debugf("=> Receiving %#v", msg) | ||||
|  | ||||
| 	// get the chatid | ||||
| 	chatid, err := strconv.ParseInt(msg.Channel, 10, 64) | ||||
| 	chatid, topicid, err := b.getIds(msg.Channel) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| @@ -84,7 +131,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { | ||||
| 	} | ||||
|  | ||||
| 	if b.GetString("MessageFormat") == HTMLFormat { | ||||
| 		msg.Text = makeHTML(msg.Text) | ||||
| 		msg.Text = makeHTML(html.EscapeString(msg.Text)) | ||||
| 	} | ||||
|  | ||||
| 	// Delete message | ||||
| @@ -92,16 +139,27 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { | ||||
| 		return b.handleDelete(&msg, chatid) | ||||
| 	} | ||||
|  | ||||
| 	// Handle prefix hint for unthreaded messages. | ||||
| 	if msg.ParentNotFound() { | ||||
| 		msg.ParentID = "" | ||||
| 		msg.Text = fmt.Sprintf("[reply]: %s", msg.Text) | ||||
| 	} | ||||
|  | ||||
| 	var parentID int | ||||
| 	if msg.ParentID != "" { | ||||
| 		parentID, _ = b.intParentID(msg.ParentID) | ||||
| 	} | ||||
|  | ||||
| 	// Upload a file if it exists | ||||
| 	if msg.Extra != nil { | ||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | ||||
| 			if _, msgErr := b.sendMessage(chatid, rmsg.Username, rmsg.Text); msgErr != nil { | ||||
| 			if _, msgErr := b.sendMessage(chatid, topicid, rmsg.Username, rmsg.Text, parentID); msgErr != nil { | ||||
| 				b.Log.Errorf("sendMessage failed: %s", msgErr) | ||||
| 			} | ||||
| 		} | ||||
| 		// check if we have files to upload (from slack, telegram or mattermost) | ||||
| 		if len(msg.Extra["file"]) > 0 { | ||||
| 			b.handleUploadFile(&msg, chatid) | ||||
| 			return b.handleUploadFile(&msg, chatid, topicid, parentID) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -115,7 +173,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { | ||||
| 	// Ignore empty text field needs for prevent double messages from whatsapp to telegram | ||||
| 	// when sending media with text caption | ||||
| 	if msg.Text != "" { | ||||
| 		return b.sendMessage(chatid, msg.Username, msg.Text) | ||||
| 		return b.sendMessage(chatid, topicid, msg.Username, msg.Text, parentID) | ||||
| 	} | ||||
|  | ||||
| 	return "", nil | ||||
| @@ -129,27 +187,13 @@ func (b *Btelegram) getFileDirectURL(id string) string { | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) { | ||||
| func (b *Btelegram) sendMessage(chatid int64, topicid int, username, text string, parentID int) (string, error) { | ||||
| 	m := tgbotapi.NewMessage(chatid, "") | ||||
| 	m.Text = username + text | ||||
| 	if b.GetString("MessageFormat") == HTMLFormat { | ||||
| 		b.Log.Debug("Using mode HTML") | ||||
| 		m.ParseMode = tgbotapi.ModeHTML | ||||
| 	m.Text, m.ParseMode = TGGetParseMode(b, username, text) | ||||
| 	if topicid != 0 { | ||||
| 		m.BaseChat.MessageThreadID = topicid | ||||
| 	} | ||||
| 	if b.GetString("MessageFormat") == "Markdown" { | ||||
| 		b.Log.Debug("Using mode markdown") | ||||
| 		m.ParseMode = tgbotapi.ModeMarkdown | ||||
| 	} | ||||
| 	if b.GetString("MessageFormat") == MarkdownV2 { | ||||
| 		b.Log.Debug("Using mode MarkdownV2") | ||||
| 		m.ParseMode = MarkdownV2 | ||||
| 	} | ||||
| 	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { | ||||
| 		b.Log.Debug("Using mode HTML - nick only") | ||||
| 		m.Text = username + html.EscapeString(text) | ||||
| 		m.ParseMode = tgbotapi.ModeHTML | ||||
| 	} | ||||
|  | ||||
| 	m.ReplyToMessageID = parentID | ||||
| 	m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview") | ||||
|  | ||||
| 	res, err := b.c.Send(m) | ||||
| @@ -159,6 +203,37 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, er | ||||
| 	return strconv.Itoa(res.MessageID), nil | ||||
| } | ||||
|  | ||||
| // sendMediaFiles native upload media files via media group | ||||
| func (b *Btelegram) sendMediaFiles(msg *config.Message, chatid int64, threadid int, parentID int, media []interface{}) (string, error) { | ||||
| 	if len(media) == 0 { | ||||
| 		return "", nil | ||||
| 	} | ||||
| 	mg := tgbotapi.MediaGroupConfig{ | ||||
| 		BaseChat: tgbotapi.BaseChat{ | ||||
| 			ChatID:           chatid, | ||||
| 			MessageThreadID:  threadid, | ||||
| 			ChannelUsername:  msg.Username, | ||||
| 			ReplyToMessageID: parentID, | ||||
| 		}, | ||||
| 		Media: media, | ||||
| 	} | ||||
| 	messages, err := b.c.SendMediaGroup(mg) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	// return first message id | ||||
| 	return strconv.Itoa(messages[0].MessageID), nil | ||||
| } | ||||
|  | ||||
| // intParentID return integer parent id for telegram message | ||||
| func (b *Btelegram) intParentID(parentID string) (int, error) { | ||||
| 	pid, err := strconv.Atoi(parentID) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	return pid, nil | ||||
| } | ||||
|  | ||||
| func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) { | ||||
| 	fi := msg.Extra["file"][0].(config.FileInfo) | ||||
| 	/* if we have a sha we have successfully uploaded the file to the media server, | ||||
|   | ||||
| @@ -34,6 +34,7 @@ type user struct { | ||||
|  | ||||
| type Bvk struct { | ||||
| 	c            *api.VK | ||||
| 	lp           *longpoll.LongPoll | ||||
| 	usernamesMap map[int]user // cache of user names and avatar URLs | ||||
| 	*bridge.Config | ||||
| } | ||||
| @@ -45,23 +46,25 @@ func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 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")) | ||||
|  | ||||
| 	var err error | ||||
| 	b.lp, err = longpoll.NewLongPollCommunity(b.c) | ||||
| 	if err != nil { | ||||
| 		b.Log.Debugf("%#v", err) | ||||
|  | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) { | ||||
| 	b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) { | ||||
| 		b.handleMessage(obj.Message, false) | ||||
| 	}) | ||||
|  | ||||
| 	b.Log.Info("Connection succeeded") | ||||
|  | ||||
| 	go func() { | ||||
| 		err := lp.Run() | ||||
| 		err := b.lp.Run() | ||||
| 		if err != nil { | ||||
| 			b.Log.Fatal("Enable longpoll in group management") | ||||
| 			b.Log.WithError(err).Fatal("Enable longpoll in group management") | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| @@ -69,6 +72,8 @@ func (b *Bvk) Connect() error { | ||||
| } | ||||
|  | ||||
| func (b *Bvk) Disconnect() error { | ||||
| 	b.lp.Shutdown() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @@ -218,7 +223,7 @@ func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, s | ||||
| 		} | ||||
| 		a, err := b.uploadFile(fi, peerID) | ||||
| 		if err != nil { | ||||
| 			b.Log.Error("File upload error ", fi.Name) | ||||
| 			b.Log.WithError(err).Error("File upload error ", fi.Name) | ||||
| 		} | ||||
|  | ||||
| 		attachments = append(attachments, a) | ||||
| @@ -232,7 +237,8 @@ func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) { | ||||
|  | ||||
| 	photoRE := regexp.MustCompile(".(jpg|jpe|png)$") | ||||
| 	if photoRE.MatchString(file.Name) { | ||||
| 		p, err := b.c.UploadMessagesPhoto(peerID, r) | ||||
| 		// BUG(VK): for community chat peerID=0 | ||||
| 		p, err := b.c.UploadMessagesPhoto(0, r) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| // nolint:goconst | ||||
| package bwhatsapp | ||||
|  | ||||
| import ( | ||||
| @@ -134,6 +135,7 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { | ||||
| } | ||||
|  | ||||
| // HandleImageMessage sent from WhatsApp, relay it to the brige | ||||
| // nolint:funlen | ||||
| func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) { | ||||
| 	if message.Info.FromMe || message.Info.Timestamp < b.startedAt { | ||||
| 		return | ||||
| @@ -175,6 +177,11 @@ func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) { | ||||
| 		fileExt[0] = ".jpg" | ||||
| 	} | ||||
|  | ||||
| 	// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463 | ||||
| 	if fileExt[0] == ".jpe" { | ||||
| 		fileExt[0] = ".jpg" | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0]) | ||||
|  | ||||
| 	b.Log.Debugf("Trying to download %s with type %s", filename, message.Type) | ||||
| @@ -232,6 +239,10 @@ func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(fileExt) == 0 { | ||||
| 		fileExt = append(fileExt, ".mp4") | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0]) | ||||
|  | ||||
| 	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type) | ||||
| @@ -289,6 +300,10 @@ func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) { | ||||
| 		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) | ||||
| @@ -308,3 +323,60 @@ func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) { | ||||
|  | ||||
| 	b.Remote <- rmsg | ||||
| } | ||||
|  | ||||
| // HandleDocumentMessage downloads documents | ||||
| func (b *Bwhatsapp) HandleDocumentMessage(message whatsapp.DocumentMessage) { | ||||
| 	if message.Info.FromMe || message.Info.Timestamp < b.startedAt { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	senderJID := message.Info.SenderJid | ||||
| 	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||
| 		senderJID = *message.Info.Source.Participant | ||||
| 	} | ||||
|  | ||||
| 	senderName := b.getSenderName(message.Info.SenderJid) | ||||
| 	if senderName == "" { | ||||
| 		senderName = "Someone" // don't expose telephone number | ||||
| 	} | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   senderJID, | ||||
| 		Username: senderName, | ||||
| 		Channel:  message.Info.RemoteJid, | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 		ID:       message.Info.Id, | ||||
| 	} | ||||
|  | ||||
| 	if avatarURL, exists := b.userAvatars[senderJID]; exists { | ||||
| 		rmsg.Avatar = avatarURL | ||||
| 	} | ||||
|  | ||||
| 	fileExt, err := mime.ExtensionsByType(message.Type) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("%v", message.FileName) | ||||
|  | ||||
| 	b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, message.Type) | ||||
|  | ||||
| 	data, err := message.Download() | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Download document message failed: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Move file to bridge storage | ||||
| 	helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General) | ||||
|  | ||||
| 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||
|  | ||||
| 	b.Remote <- rmsg | ||||
| } | ||||
|   | ||||
| @@ -111,8 +111,7 @@ func (b *Bwhatsapp) getSenderName(senderJid string) string { | ||||
| 	} | ||||
|  | ||||
| 	// try to reload this contact | ||||
| 	_, err := b.conn.Contacts() | ||||
| 	if err != nil { | ||||
| 	if _, err := b.conn.Contacts(); err != nil { | ||||
| 		b.Log.Errorf("error on update of contacts: %v", err) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -40,6 +40,11 @@ type Bwhatsapp struct { | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	number := cfg.GetString(cfgNumber) | ||||
|  | ||||
| 	cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") | ||||
| 	cfg.Log.Warn("This bridge is deprecated and not supported anymore. Use the new multidevice whatsapp bridge") | ||||
| 	cfg.Log.Warn("See https://github.com/42wim/matterbridge#building-with-whatsapp-beta-multidevice-support for more info") | ||||
| 	cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") | ||||
|  | ||||
| 	if number == "" { | ||||
| 		cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number") | ||||
| 	} | ||||
| @@ -293,7 +298,11 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) { | ||||
| 	if msg.ID != "" { | ||||
| 		b.Log.Debugf("updating message with id %s", msg.ID) | ||||
|  | ||||
| 		msg.Text += " (edited)" | ||||
| 		if b.GetString("editsuffix") != "" { | ||||
| 			msg.Text += b.GetString("EditSuffix") | ||||
| 		} else { | ||||
| 			msg.Text += " (edited)" | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Handle Upload a file | ||||
|   | ||||
							
								
								
									
										454
									
								
								bridge/whatsappmulti/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,454 @@ | ||||
| //go:build whatsappmulti | ||||
| // +build whatsappmulti | ||||
|  | ||||
| package bwhatsapp | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"mime" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
|  | ||||
| 	"go.mau.fi/whatsmeow/binary/proto" | ||||
| 	"go.mau.fi/whatsmeow/types" | ||||
| 	"go.mau.fi/whatsmeow/types/events" | ||||
| ) | ||||
|  | ||||
| // nolint:gocritic | ||||
| func (b *Bwhatsapp) eventHandler(evt interface{}) { | ||||
| 	switch e := evt.(type) { | ||||
| 	case *events.Message: | ||||
| 		b.handleMessage(e) | ||||
| 	case *events.GroupInfo: | ||||
| 		b.handleGroupInfo(e) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) handleGroupInfo(event *events.GroupInfo) { | ||||
| 	b.Log.Debugf("Receiving event %#v", event) | ||||
|  | ||||
| 	switch { | ||||
| 	case event.Join != nil: | ||||
| 		b.handleUserJoin(event) | ||||
| 	case event.Leave != nil: | ||||
| 		b.handleUserLeave(event) | ||||
| 	case event.Topic != nil: | ||||
| 		b.handleTopicChange(event) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) handleUserJoin(event *events.GroupInfo) { | ||||
| 	for _, joinedJid := range event.Join { | ||||
| 		senderName := b.getSenderNameFromJID(joinedJid) | ||||
|  | ||||
| 		rmsg := config.Message{ | ||||
| 			UserID:   joinedJid.String(), | ||||
| 			Username: senderName, | ||||
| 			Channel:  event.JID.String(), | ||||
| 			Account:  b.Account, | ||||
| 			Protocol: b.Protocol, | ||||
| 			Event:    config.EventJoinLeave, | ||||
| 			Text:     "joined chat", | ||||
| 		} | ||||
|  | ||||
| 		b.Remote <- rmsg | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) handleUserLeave(event *events.GroupInfo) { | ||||
| 	for _, leftJid := range event.Leave { | ||||
| 		senderName := b.getSenderNameFromJID(leftJid) | ||||
|  | ||||
| 		rmsg := config.Message{ | ||||
| 			UserID:   leftJid.String(), | ||||
| 			Username: senderName, | ||||
| 			Channel:  event.JID.String(), | ||||
| 			Account:  b.Account, | ||||
| 			Protocol: b.Protocol, | ||||
| 			Event:    config.EventJoinLeave, | ||||
| 			Text:     "left chat", | ||||
| 		} | ||||
|  | ||||
| 		b.Remote <- rmsg | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) handleTopicChange(event *events.GroupInfo) { | ||||
| 	msg := event.Topic | ||||
| 	senderJid := msg.TopicSetBy | ||||
| 	senderName := b.getSenderNameFromJID(senderJid) | ||||
|  | ||||
| 	text := msg.Topic | ||||
| 	if text == "" { | ||||
| 		text = "removed topic" | ||||
| 	} | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   senderJid.String(), | ||||
| 		Username: senderName, | ||||
| 		Channel:  event.JID.String(), | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Event:    config.EventTopicChange, | ||||
| 		Text:     "Topic changed: " + text, | ||||
| 	} | ||||
|  | ||||
| 	b.Remote <- rmsg | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) handleMessage(message *events.Message) { | ||||
| 	msg := message.Message | ||||
| 	switch { | ||||
| 	case msg == nil, message.Info.IsFromMe, message.Info.Timestamp.Before(b.startedAt): | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("Receiving message %#v", msg) | ||||
|  | ||||
| 	switch { | ||||
| 	case msg.Conversation != nil || msg.ExtendedTextMessage != nil: | ||||
| 		b.handleTextMessage(message.Info, msg) | ||||
| 	case msg.VideoMessage != nil: | ||||
| 		b.handleVideoMessage(message) | ||||
| 	case msg.AudioMessage != nil: | ||||
| 		b.handleAudioMessage(message) | ||||
| 	case msg.DocumentMessage != nil: | ||||
| 		b.handleDocumentMessage(message) | ||||
| 	case msg.ImageMessage != nil: | ||||
| 		b.handleImageMessage(message) | ||||
| 	case msg.ProtocolMessage != nil && *msg.ProtocolMessage.Type == proto.ProtocolMessage_REVOKE: | ||||
| 		b.handleDelete(msg.ProtocolMessage) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // nolint:funlen | ||||
| func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.Message) { | ||||
| 	senderJID := messageInfo.Sender | ||||
| 	channel := messageInfo.Chat | ||||
|  | ||||
| 	senderName := b.getSenderName(messageInfo) | ||||
|  | ||||
| 	if msg.GetExtendedTextMessage() == nil && msg.GetConversation() == "" { | ||||
| 		b.Log.Debugf("message without text content? %#v", msg) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var text string | ||||
|  | ||||
| 	// nolint:nestif | ||||
| 	if msg.GetExtendedTextMessage() == nil { | ||||
| 		text = msg.GetConversation() | ||||
| 	} else if msg.GetExtendedTextMessage().GetContextInfo() == nil { | ||||
| 		// Handle pure text message with a link preview | ||||
| 		// A pure text message with a link preview acts as an extended text message but will not contain any context info | ||||
| 		text = msg.GetExtendedTextMessage().GetText() | ||||
| 	} else { | ||||
| 		text = msg.GetExtendedTextMessage().GetText() | ||||
| 		ci := msg.GetExtendedTextMessage().GetContextInfo() | ||||
|  | ||||
| 		if senderJID == (types.JID{}) && ci.Participant != nil { | ||||
| 			senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) | ||||
| 		} | ||||
|  | ||||
| 		if ci.MentionedJID != nil { | ||||
| 			// handle user mentions | ||||
| 			for _, mentionedJID := range ci.MentionedJID { | ||||
| 				numberAndSuffix := strings.SplitN(mentionedJID, "@", 2) | ||||
|  | ||||
| 				// mentions comes as telephone numbers and we don't want to expose it to other bridges | ||||
| 				// replace it with something more meaninful to others | ||||
| 				mention := b.getSenderNotify(types.NewJID(numberAndSuffix[0], types.DefaultUserServer)) | ||||
|  | ||||
| 				text = strings.Replace(text, "@"+numberAndSuffix[0], "@"+mention, 1) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	parentID := "" | ||||
| 	if msg.GetExtendedTextMessage() != nil { | ||||
| 		ci := msg.GetExtendedTextMessage().GetContextInfo() | ||||
| 		parentID = getParentIdFromCtx(ci) | ||||
| 	} | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   senderJID.String(), | ||||
| 		Username: senderName, | ||||
| 		Text:     text, | ||||
| 		Channel:  channel.String(), | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 		ID:       getMessageIdFormat(senderJID, messageInfo.ID), | ||||
| 		ParentID: parentID, | ||||
| 	} | ||||
|  | ||||
| 	if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { | ||||
| 		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.Remote <- rmsg | ||||
| } | ||||
|  | ||||
| // HandleImageMessage sent from WhatsApp, relay it to the brige | ||||
| func (b *Bwhatsapp) handleImageMessage(msg *events.Message) { | ||||
| 	imsg := msg.Message.GetImageMessage() | ||||
|  | ||||
| 	senderJID := msg.Info.Sender | ||||
| 	senderName := b.getSenderName(msg.Info) | ||||
| 	ci := imsg.GetContextInfo() | ||||
|  | ||||
| 	if senderJID == (types.JID{}) && ci.Participant != nil { | ||||
| 		senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) | ||||
| 	} | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   senderJID.String(), | ||||
| 		Username: senderName, | ||||
| 		Channel:  msg.Info.Chat.String(), | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 		ID:       getMessageIdFormat(senderJID, msg.Info.ID), | ||||
| 		ParentID: getParentIdFromCtx(ci), | ||||
| 	} | ||||
|  | ||||
| 	if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { | ||||
| 		rmsg.Avatar = avatarURL | ||||
| 	} | ||||
|  | ||||
| 	fileExt, err := mime.ExtensionsByType(imsg.GetMimetype()) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292 | ||||
| 	if fileExt[0] == ".jfif" { | ||||
| 		fileExt[0] = ".jpg" | ||||
| 	} | ||||
|  | ||||
| 	// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463 | ||||
| 	if fileExt[0] == ".jpe" { | ||||
| 		fileExt[0] = ".jpg" | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0]) | ||||
|  | ||||
| 	b.Log.Debugf("Trying to download %s with type %s", filename, imsg.GetMimetype()) | ||||
|  | ||||
| 	data, err := b.wc.Download(imsg) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Download image failed: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Move file to bridge storage | ||||
| 	helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &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(msg *events.Message) { | ||||
| 	imsg := msg.Message.GetVideoMessage() | ||||
|  | ||||
| 	senderJID := msg.Info.Sender | ||||
| 	senderName := b.getSenderName(msg.Info) | ||||
| 	ci := imsg.GetContextInfo() | ||||
|  | ||||
| 	if senderJID == (types.JID{}) && ci.Participant != nil { | ||||
| 		senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) | ||||
| 	} | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   senderJID.String(), | ||||
| 		Username: senderName, | ||||
| 		Channel:  msg.Info.Chat.String(), | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 		ID:       getMessageIdFormat(senderJID, msg.Info.ID), | ||||
| 		ParentID: getParentIdFromCtx(ci), | ||||
| 	} | ||||
|  | ||||
| 	if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { | ||||
| 		rmsg.Avatar = avatarURL | ||||
| 	} | ||||
|  | ||||
| 	fileExt, err := mime.ExtensionsByType(imsg.GetMimetype()) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(fileExt) == 0 { | ||||
| 		fileExt = append(fileExt, ".mp4") | ||||
| 	} | ||||
|  | ||||
| 	// Prefer .mp4 extension, otherwise fallback to first index | ||||
| 	fileExtIndex := 0 | ||||
| 	for i, n := range fileExt { | ||||
| 		if ".mp4" == n { | ||||
| 			fileExtIndex = i | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[fileExtIndex]) | ||||
|  | ||||
| 	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype()) | ||||
|  | ||||
| 	data, err := b.wc.Download(imsg) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Download video failed: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Move file to bridge storage | ||||
| 	helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &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(msg *events.Message) { | ||||
| 	imsg := msg.Message.GetAudioMessage() | ||||
|  | ||||
| 	senderJID := msg.Info.Sender | ||||
| 	senderName := b.getSenderName(msg.Info) | ||||
| 	ci := imsg.GetContextInfo() | ||||
|  | ||||
| 	if senderJID == (types.JID{}) && ci.Participant != nil { | ||||
| 		senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) | ||||
| 	} | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   senderJID.String(), | ||||
| 		Username: senderName, | ||||
| 		Channel:  msg.Info.Chat.String(), | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 		ID:       getMessageIdFormat(senderJID, msg.Info.ID), | ||||
| 		ParentID: getParentIdFromCtx(ci), | ||||
| 	} | ||||
|  | ||||
| 	if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { | ||||
| 		rmsg.Avatar = avatarURL | ||||
| 	} | ||||
|  | ||||
| 	fileExt, err := mime.ExtensionsByType(imsg.GetMimetype()) | ||||
| 	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", msg.Info.ID, fileExt[0]) | ||||
|  | ||||
| 	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype()) | ||||
|  | ||||
| 	data, err := b.wc.Download(imsg) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Download video failed: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Move file to bridge storage | ||||
| 	helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General) | ||||
|  | ||||
| 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||
|  | ||||
| 	b.Remote <- rmsg | ||||
| } | ||||
|  | ||||
| // HandleDocumentMessage downloads documents | ||||
| func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) { | ||||
| 	imsg := msg.Message.GetDocumentMessage() | ||||
|  | ||||
| 	senderJID := msg.Info.Sender | ||||
| 	senderName := b.getSenderName(msg.Info) | ||||
| 	ci := imsg.GetContextInfo() | ||||
|  | ||||
| 	if senderJID == (types.JID{}) && ci.Participant != nil { | ||||
| 		senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) | ||||
| 	} | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   senderJID.String(), | ||||
| 		Username: senderName, | ||||
| 		Channel:  msg.Info.Chat.String(), | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 		ID:       getMessageIdFormat(senderJID, msg.Info.ID), | ||||
| 		ParentID: getParentIdFromCtx(ci), | ||||
| 	} | ||||
|  | ||||
| 	if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { | ||||
| 		rmsg.Avatar = avatarURL | ||||
| 	} | ||||
|  | ||||
| 	fileExt, err := mime.ExtensionsByType(imsg.GetMimetype()) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("%v", imsg.GetFileName()) | ||||
|  | ||||
| 	b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, imsg.GetMimetype()) | ||||
|  | ||||
| 	data, err := b.wc.Download(imsg) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Download document message failed: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Move file to bridge storage | ||||
| 	helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &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 | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) handleDelete(messageInfo *proto.ProtocolMessage) { | ||||
| 	sender, _ := types.ParseJID(*messageInfo.Key.Participant) | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		ID:       getMessageIdFormat(sender, *messageInfo.Key.ID), | ||||
| 		Event:    config.EventMsgDelete, | ||||
| 		Text:     config.EventMsgDelete, | ||||
| 		Channel:  *messageInfo.Key.RemoteJID, | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("<= Sending message from %s to gateway", b.Account) | ||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||
| 	b.Remote <- rmsg | ||||
| } | ||||
							
								
								
									
										209
									
								
								bridge/whatsappmulti/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,209 @@ | ||||
| //go:build whatsappmulti | ||||
| // +build whatsappmulti | ||||
|  | ||||
| package bwhatsapp | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	goproto "google.golang.org/protobuf/proto" | ||||
|  | ||||
| 	"go.mau.fi/whatsmeow" | ||||
| 	"go.mau.fi/whatsmeow/binary/proto" | ||||
| 	"go.mau.fi/whatsmeow/store" | ||||
| 	"go.mau.fi/whatsmeow/store/sqlstore" | ||||
| 	"go.mau.fi/whatsmeow/types" | ||||
| ) | ||||
|  | ||||
| type ProfilePicInfo struct { | ||||
| 	URL    string `json:"eurl"` | ||||
| 	Tag    string `json:"tag"` | ||||
| 	Status int16  `json:"status"` | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) reloadContacts() { | ||||
| 	if _, err := b.wc.Store.Contacts.GetAllContacts(); err != nil { | ||||
| 		b.Log.Errorf("error on update of contacts: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	allcontacts, err := b.wc.Store.Contacts.GetAllContacts() | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("error on update of contacts: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if len(allcontacts) > 0 { | ||||
| 		b.contacts = allcontacts | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) getSenderName(info types.MessageInfo) string { | ||||
| 	// Parse AD JID | ||||
| 	var senderJid types.JID | ||||
| 	senderJid.User, senderJid.Server = info.Sender.User, info.Sender.Server | ||||
|  | ||||
| 	sender, exists := b.contacts[senderJid] | ||||
|  | ||||
| 	if !exists || (sender.FullName == "" && sender.FirstName == "") { | ||||
| 		b.reloadContacts() // Contacts may need to be reloaded | ||||
| 		sender, exists = b.contacts[senderJid] | ||||
| 	} | ||||
|  | ||||
| 	if exists && sender.FullName != "" { | ||||
| 		return sender.FullName | ||||
| 	} | ||||
|  | ||||
| 	if info.PushName != "" { | ||||
| 		return info.PushName | ||||
| 	} | ||||
|  | ||||
| 	if exists && sender.FirstName != "" { | ||||
| 		return sender.FirstName | ||||
| 	} | ||||
|  | ||||
| 	return "Someone" | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) getSenderNameFromJID(senderJid types.JID) string { | ||||
| 	sender, exists := b.contacts[senderJid] | ||||
|  | ||||
| 	if !exists || (sender.FullName == "" && sender.FirstName == "") { | ||||
| 		b.reloadContacts() // Contacts may need to be reloaded | ||||
| 		sender, exists = b.contacts[senderJid] | ||||
| 	} | ||||
|  | ||||
| 	if exists && sender.FullName != "" { | ||||
| 		return sender.FullName | ||||
| 	} | ||||
|  | ||||
| 	if exists && sender.FirstName != "" { | ||||
| 		return sender.FirstName | ||||
| 	} | ||||
|  | ||||
| 	if sender.PushName != "" { | ||||
| 		return sender.PushName | ||||
| 	} | ||||
|  | ||||
| 	return "Someone" | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) getSenderNotify(senderJid types.JID) string { | ||||
| 	sender, exists := b.contacts[senderJid] | ||||
|  | ||||
| 	if !exists || (sender.FullName == "" && sender.PushName == "" && sender.FirstName == "") { | ||||
| 		b.reloadContacts() // Contacts may need to be reloaded | ||||
| 		sender, exists = b.contacts[senderJid] | ||||
| 	} | ||||
|  | ||||
| 	if !exists { | ||||
| 		return "someone" | ||||
| 	} | ||||
|  | ||||
| 	if exists && sender.FullName != "" { | ||||
| 		return sender.FullName | ||||
| 	} | ||||
|  | ||||
| 	if exists && sender.PushName != "" { | ||||
| 		return sender.PushName | ||||
| 	} | ||||
|  | ||||
| 	if exists && sender.FirstName != "" { | ||||
| 		return sender.FirstName | ||||
| 	} | ||||
|  | ||||
| 	return "someone" | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*types.ProfilePictureInfo, error) { | ||||
| 	pjid, _ := types.ParseJID(jid) | ||||
|  | ||||
| 	info, err := b.wc.GetProfilePictureInfo(pjid, &whatsmeow.GetProfilePictureParams{ | ||||
| 		Preview: true, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get avatar: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return info, nil | ||||
| } | ||||
|  | ||||
| func isGroupJid(identifier string) bool { | ||||
| 	return strings.HasSuffix(identifier, "@g.us") || | ||||
| 		strings.HasSuffix(identifier, "@temp") || | ||||
| 		strings.HasSuffix(identifier, "@broadcast") | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) getDevice() (*store.Device, error) { | ||||
| 	device := &store.Device{} | ||||
|  | ||||
| 	storeContainer, err := sqlstore.New("sqlite", "file:"+b.Config.GetString("sessionfile")+".db?_pragma=foreign_keys(1)&_pragma=busy_timeout=10000", nil) | ||||
| 	if err != nil { | ||||
| 		return device, fmt.Errorf("failed to connect to database: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	device, err = storeContainer.GetFirstDevice() | ||||
| 	if err != nil { | ||||
| 		return device, fmt.Errorf("failed to get device: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return device, nil | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) getNewReplyContext(parentID string) (*proto.ContextInfo, error) { | ||||
| 	replyInfo, err := b.parseMessageID(parentID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	sender := fmt.Sprintf("%s@%s", replyInfo.Sender.User, replyInfo.Sender.Server) | ||||
| 	ctx := &proto.ContextInfo{ | ||||
| 		StanzaID:      &replyInfo.MessageID, | ||||
| 		Participant:   &sender, | ||||
| 		QuotedMessage: &proto.Message{Conversation: goproto.String("")}, | ||||
| 	} | ||||
|  | ||||
| 	return ctx, nil | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) parseMessageID(id string) (*Replyable, error) { | ||||
| 	// 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 | ||||
| 	if id == "" { | ||||
| 		return &Replyable{MessageID: id}, nil | ||||
| 	} | ||||
|  | ||||
| 	replyInfo := strings.Split(id, "/") | ||||
|  | ||||
| 	if len(replyInfo) == 2 { | ||||
| 		sender, err := types.ParseJID(replyInfo[0]) | ||||
|  | ||||
| 		if err == nil { | ||||
| 			return &Replyable{ | ||||
| 				MessageID: types.MessageID(replyInfo[1]), | ||||
| 				Sender:    sender, | ||||
| 			}, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	err := fmt.Errorf("MessageID does not match format of {senderJID}:{messageID} : \"%s\"", id) | ||||
|  | ||||
| 	return &Replyable{MessageID: id}, err | ||||
| } | ||||
|  | ||||
| func getParentIdFromCtx(ci *proto.ContextInfo) string { | ||||
| 	if ci != nil && ci.StanzaID != nil { | ||||
| 		senderJid, err := types.ParseJID(*ci.Participant) | ||||
|  | ||||
| 		if err == nil { | ||||
| 			return getMessageIdFormat(senderJid, *ci.StanzaID) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func getMessageIdFormat(jid types.JID, messageID string) string { | ||||
| 	// we're crafting our own JID str as AD JID format messes with how stuff looks on a webclient | ||||
| 	jidStr := fmt.Sprintf("%s@%s", jid.User, jid.Server) | ||||
| 	return fmt.Sprintf("%s/%s", jidStr, messageID) | ||||
| } | ||||
							
								
								
									
										456
									
								
								bridge/whatsappmulti/whatsapp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,456 @@ | ||||
| //go:build whatsappmulti | ||||
| // +build whatsappmulti | ||||
|  | ||||
| package bwhatsapp | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"mime" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/mdp/qrterminal" | ||||
|  | ||||
| 	"go.mau.fi/whatsmeow" | ||||
| 	"go.mau.fi/whatsmeow/binary/proto" | ||||
| 	"go.mau.fi/whatsmeow/types" | ||||
| 	waLog "go.mau.fi/whatsmeow/util/log" | ||||
|  | ||||
| 	goproto "google.golang.org/protobuf/proto" | ||||
|  | ||||
| 	_ "modernc.org/sqlite" // needed for sqlite | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// Account config parameters | ||||
| 	cfgNumber = "Number" | ||||
| ) | ||||
|  | ||||
| // Bwhatsapp Bridge structure keeping all the information needed for relying | ||||
| type Bwhatsapp struct { | ||||
| 	*bridge.Config | ||||
|  | ||||
| 	startedAt    time.Time | ||||
| 	wc           *whatsmeow.Client | ||||
| 	contacts     map[types.JID]types.ContactInfo | ||||
| 	users        map[string]types.ContactInfo | ||||
| 	userAvatars  map[string]string | ||||
| 	joinedGroups []*types.GroupInfo | ||||
| } | ||||
|  | ||||
| type Replyable struct { | ||||
| 	MessageID types.MessageID | ||||
| 	Sender    types.JID | ||||
| } | ||||
|  | ||||
| // New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	number := cfg.GetString(cfgNumber) | ||||
|  | ||||
| 	if number == "" { | ||||
| 		cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number") | ||||
| 	} | ||||
|  | ||||
| 	b := &Bwhatsapp{ | ||||
| 		Config: cfg, | ||||
|  | ||||
| 		users:       make(map[string]types.ContactInfo), | ||||
| 		userAvatars: make(map[string]string), | ||||
| 	} | ||||
|  | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| // Connect to WhatsApp. Required implementation of the Bridger interface | ||||
| func (b *Bwhatsapp) Connect() error { | ||||
| 	device, err := b.getDevice() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	number := b.GetString(cfgNumber) | ||||
| 	if number == "" { | ||||
| 		return errors.New("whatsapp's telephone number need to be configured") | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugln("Connecting to WhatsApp..") | ||||
|  | ||||
| 	b.wc = whatsmeow.NewClient(device, waLog.Stdout("Client", "INFO", true)) | ||||
| 	b.wc.AddEventHandler(b.eventHandler) | ||||
|  | ||||
| 	firstlogin := false | ||||
| 	var qrChan <-chan whatsmeow.QRChannelItem | ||||
| 	if b.wc.Store.ID == nil { | ||||
| 		firstlogin = true | ||||
| 		qrChan, err = b.wc.GetQRChannel(context.Background()) | ||||
| 		if err != nil && !errors.Is(err, whatsmeow.ErrQRStoreContainsID) { | ||||
| 			return errors.New("failed to to get QR channel:" + err.Error()) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	err = b.wc.Connect() | ||||
| 	if err != nil { | ||||
| 		return errors.New("failed to connect to WhatsApp: " + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if b.wc.Store.ID == nil { | ||||
| 		for evt := range qrChan { | ||||
| 			if evt.Event == "code" { | ||||
| 				qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) | ||||
| 			} else { | ||||
| 				b.Log.Infof("QR channel result: %s", evt.Event) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// disconnect and reconnect on our first login/pairing | ||||
| 	// for some reason the GetJoinedGroups in JoinChannel doesn't work on first login | ||||
| 	if firstlogin { | ||||
| 		b.wc.Disconnect() | ||||
| 		time.Sleep(time.Second) | ||||
|  | ||||
| 		err = b.wc.Connect() | ||||
| 		if err != nil { | ||||
| 			return errors.New("failed to connect to WhatsApp: " + err.Error()) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Infoln("WhatsApp connection successful") | ||||
|  | ||||
| 	b.contacts, err = b.wc.Store.Contacts.GetAllContacts() | ||||
| 	if err != nil { | ||||
| 		return errors.New("failed to get contacts: " + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	b.joinedGroups, err = b.wc.GetJoinedGroups() | ||||
| 	if err != nil { | ||||
| 		return errors.New("failed to get list of joined groups: " + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	b.startedAt = time.Now() | ||||
|  | ||||
| 	// map all the users | ||||
| 	for id, contact := range b.contacts { | ||||
| 		if !isGroupJid(id.String()) && id.String() != "status@broadcast" { | ||||
| 			// it is user | ||||
| 			b.users[id.String()] = contact | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// get user avatar asynchronously | ||||
| 	b.Log.Info("Getting user avatars..") | ||||
|  | ||||
| 	for jid := range b.users { | ||||
| 		info, err := b.GetProfilePicThumb(jid) | ||||
| 		if err != nil { | ||||
| 			b.Log.Warnf("Could not get profile photo of %s: %v", jid, err) | ||||
| 		} else { | ||||
| 			b.Lock() | ||||
| 			if info != nil { | ||||
| 				b.userAvatars[jid] = info.URL | ||||
| 			} | ||||
| 			b.Unlock() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Info("Finished getting avatars..") | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Disconnect is called while reconnecting to the bridge | ||||
| // Required implementation of the Bridger interface | ||||
| func (b *Bwhatsapp) Disconnect() error { | ||||
| 	b.wc.Disconnect() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name' | ||||
| // Required implementation of the Bridger interface | ||||
| // https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16 | ||||
| func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	byJid := isGroupJid(channel.Name) | ||||
|  | ||||
| 	// verify if we are member of the given group | ||||
| 	if byJid { | ||||
| 		gJID, err := types.ParseJID(channel.Name) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		for _, group := range b.joinedGroups { | ||||
| 			if group.JID == gJID { | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	foundGroups := []string{} | ||||
|  | ||||
| 	for _, group := range b.joinedGroups { | ||||
| 		if group.Name == channel.Name { | ||||
| 			foundGroups = append(foundGroups, group.Name) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	switch len(foundGroups) { | ||||
| 	case 0: | ||||
| 		// didn't match any group - print out possibilites | ||||
| 		for _, group := range b.joinedGroups { | ||||
| 			b.Log.Infof("%s %s", group.JID, group.Name) | ||||
| 		} | ||||
| 		return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name) | ||||
| 	case 1: | ||||
| 		return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", foundGroups[0], channel.Name) | ||||
| 	default: | ||||
| 		return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, foundGroups) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Post a document message from the bridge to WhatsApp | ||||
| func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) { | ||||
| 	groupJID, _ := types.ParseJID(msg.Channel) | ||||
|  | ||||
| 	fi := msg.Extra["file"][0].(config.FileInfo) | ||||
|  | ||||
| 	caption := msg.Username + fi.Comment | ||||
|  | ||||
| 	resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaDocument) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	// Post document message | ||||
| 	var message proto.Message | ||||
| 	var ctx *proto.ContextInfo | ||||
| 	if msg.ParentID != "" { | ||||
| 		ctx, _ = b.getNewReplyContext(msg.ParentID) | ||||
| 	} | ||||
|  | ||||
| 	message.DocumentMessage = &proto.DocumentMessage{ | ||||
| 		Title:         &fi.Name, | ||||
| 		FileName:      &fi.Name, | ||||
| 		Mimetype:      &filetype, | ||||
| 		Caption:       &caption, | ||||
| 		MediaKey:      resp.MediaKey, | ||||
| 		FileEncSHA256: resp.FileEncSHA256, | ||||
| 		FileSHA256:    resp.FileSHA256, | ||||
| 		FileLength:    goproto.Uint64(resp.FileLength), | ||||
| 		URL:           &resp.URL, | ||||
| 		DirectPath:    &resp.DirectPath, | ||||
| 		ContextInfo:   ctx, | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("=> Sending %#v as a document", msg) | ||||
|  | ||||
| 	ID := whatsmeow.GenerateMessageID() | ||||
| 	_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID}) | ||||
|  | ||||
| 	return 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) | ||||
|  | ||||
| 	caption := msg.Username + fi.Comment | ||||
|  | ||||
| 	resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaImage) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	var message proto.Message | ||||
| 	var ctx *proto.ContextInfo | ||||
| 	if msg.ParentID != "" { | ||||
| 		ctx, _ = b.getNewReplyContext(msg.ParentID) | ||||
| 	} | ||||
|  | ||||
| 	message.ImageMessage = &proto.ImageMessage{ | ||||
| 		Mimetype:      &filetype, | ||||
| 		Caption:       &caption, | ||||
| 		MediaKey:      resp.MediaKey, | ||||
| 		FileEncSHA256: resp.FileEncSHA256, | ||||
| 		FileSHA256:    resp.FileSHA256, | ||||
| 		FileLength:    goproto.Uint64(resp.FileLength), | ||||
| 		URL:           &resp.URL, | ||||
| 		DirectPath:    &resp.DirectPath, | ||||
| 		ContextInfo:   ctx, | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("=> Sending %#v as an image", msg) | ||||
|  | ||||
| 	return b.sendMessage(msg, &message) | ||||
| } | ||||
|  | ||||
| // Post a video message from the bridge to WhatsApp | ||||
| func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (string, error) { | ||||
| 	fi := msg.Extra["file"][0].(config.FileInfo) | ||||
|  | ||||
| 	caption := msg.Username + fi.Comment | ||||
|  | ||||
| 	resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaVideo) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	var message proto.Message | ||||
| 	var ctx *proto.ContextInfo | ||||
| 	if msg.ParentID != "" { | ||||
| 		ctx, _ = b.getNewReplyContext(msg.ParentID) | ||||
| 	} | ||||
|  | ||||
| 	message.VideoMessage = &proto.VideoMessage{ | ||||
| 		Mimetype:      &filetype, | ||||
| 		Caption:       &caption, | ||||
| 		MediaKey:      resp.MediaKey, | ||||
| 		FileEncSHA256: resp.FileEncSHA256, | ||||
| 		FileSHA256:    resp.FileSHA256, | ||||
| 		FileLength:    goproto.Uint64(resp.FileLength), | ||||
| 		URL:           &resp.URL, | ||||
| 		DirectPath:    &resp.DirectPath, | ||||
| 		ContextInfo:   ctx, | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("=> Sending %#v as a video", msg) | ||||
|  | ||||
| 	return b.sendMessage(msg, &message) | ||||
| } | ||||
|  | ||||
| // Post audio inline | ||||
| func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (string, error) { | ||||
| 	groupJID, _ := types.ParseJID(msg.Channel) | ||||
|  | ||||
| 	fi := msg.Extra["file"][0].(config.FileInfo) | ||||
|  | ||||
| 	resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaAudio) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	var message proto.Message | ||||
| 	var ctx *proto.ContextInfo | ||||
| 	if msg.ParentID != "" { | ||||
| 		ctx, _ = b.getNewReplyContext(msg.ParentID) | ||||
| 	} | ||||
|  | ||||
| 	message.AudioMessage = &proto.AudioMessage{ | ||||
| 		Mimetype:      &filetype, | ||||
| 		MediaKey:      resp.MediaKey, | ||||
| 		FileEncSHA256: resp.FileEncSHA256, | ||||
| 		FileSHA256:    resp.FileSHA256, | ||||
| 		FileLength:    goproto.Uint64(resp.FileLength), | ||||
| 		URL:           &resp.URL, | ||||
| 		DirectPath:    &resp.DirectPath, | ||||
| 		ContextInfo:   ctx, | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("=> Sending %#v as audio", msg) | ||||
|  | ||||
| 	ID, err := b.sendMessage(msg, &message) | ||||
|  | ||||
| 	var captionMessage proto.Message | ||||
| 	caption := msg.Username + fi.Comment + "\u2B06" // the char on the end is upwards arrow emoji | ||||
| 	captionMessage.Conversation = &caption | ||||
|  | ||||
| 	captionID := whatsmeow.GenerateMessageID() | ||||
| 	_, err = b.wc.SendMessage(context.TODO(), groupJID, &captionMessage, whatsmeow.SendRequestExtra{ID: captionID}) | ||||
|  | ||||
| 	return ID, err | ||||
| } | ||||
|  | ||||
| // Send a message from the bridge to WhatsApp | ||||
| func (b *Bwhatsapp) Send(msg config.Message) (string, error) { | ||||
| 	groupJID, _ := types.ParseJID(msg.Channel) | ||||
|  | ||||
| 	extendedMsgID, _ := b.parseMessageID(msg.ID) | ||||
| 	msg.ID = extendedMsgID.MessageID | ||||
|  | ||||
| 	b.Log.Debugf("=> Receiving %#v", msg) | ||||
|  | ||||
| 	// Delete message | ||||
| 	if msg.Event == config.EventMsgDelete { | ||||
| 		if msg.ID == "" { | ||||
| 			// No message ID in case action is executed on a message sent before the bridge was started | ||||
| 			// and then the bridge cache doesn't have this message ID mapped | ||||
| 			return "", nil | ||||
| 		} | ||||
|  | ||||
| 		_, err := b.wc.RevokeMessage(groupJID, msg.ID) | ||||
|  | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	// Edit message | ||||
| 	if msg.ID != "" { | ||||
| 		b.Log.Debugf("updating message with id %s", msg.ID) | ||||
|  | ||||
| 		if b.GetString("editsuffix") != "" { | ||||
| 			msg.Text += b.GetString("EditSuffix") | ||||
| 		} else { | ||||
| 			msg.Text += " (edited)" | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Handle Upload a file | ||||
| 	if msg.Extra["file"] != nil { | ||||
| 		fi := msg.Extra["file"][0].(config.FileInfo) | ||||
| 		filetype := mime.TypeByExtension(filepath.Ext(fi.Name)) | ||||
|  | ||||
| 		b.Log.Debugf("Extra file is %#v", filetype) | ||||
|  | ||||
| 		// TODO: add different types | ||||
| 		// TODO: add webp conversion | ||||
| 		switch filetype { | ||||
| 		case "image/jpeg", "image/png", "image/gif": | ||||
| 			return b.PostImageMessage(msg, filetype) | ||||
| 		case "video/mp4", "video/3gpp": // TODO: Check if codecs are supported by WA | ||||
| 			return b.PostVideoMessage(msg, filetype) | ||||
| 		case "audio/ogg": | ||||
| 			return b.PostAudioMessage(msg, "audio/ogg; codecs=opus") // TODO: Detect if it is actually OPUS | ||||
| 		case "audio/aac", "audio/mp4", "audio/amr", "audio/mpeg": | ||||
| 			return b.PostAudioMessage(msg, filetype) | ||||
| 		default: | ||||
| 			return b.PostDocumentMessage(msg, filetype) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var message proto.Message | ||||
| 	text := msg.Username + msg.Text | ||||
|  | ||||
| 	// If we have a parent ID send an extended message | ||||
| 	if msg.ParentID != "" { | ||||
| 		replyContext, err := b.getNewReplyContext(msg.ParentID) | ||||
|  | ||||
| 		if err == nil { | ||||
| 			message = proto.Message{ | ||||
| 				ExtendedTextMessage: &proto.ExtendedTextMessage{ | ||||
| 					Text:        &text, | ||||
| 					ContextInfo: replyContext, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			return b.sendMessage(msg, &message) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	message.Conversation = &text | ||||
|  | ||||
| 	return b.sendMessage(msg, &message) | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) sendMessage(rmsg config.Message, message *proto.Message) (string, error) { | ||||
| 	groupJID, _ := types.ParseJID(rmsg.Channel) | ||||
| 	ID := whatsmeow.GenerateMessageID() | ||||
|  | ||||
| 	_, err := b.wc.SendMessage(context.Background(), groupJID, message, whatsmeow.SendRequestExtra{ID: ID}) | ||||
|  | ||||
| 	return getMessageIdFormat(*b.wc.Store.ID, ID), err | ||||
| } | ||||
| @@ -6,6 +6,7 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| @@ -13,6 +14,7 @@ import ( | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	lru "github.com/hashicorp/golang-lru" | ||||
| 	"github.com/jpillora/backoff" | ||||
| 	"github.com/matterbridge/go-xmpp" | ||||
| 	"github.com/rs/xid" | ||||
| @@ -27,13 +29,20 @@ type Bxmpp struct { | ||||
| 	connected bool | ||||
| 	sync.RWMutex | ||||
|  | ||||
| 	StanzaIDs          *lru.Cache | ||||
| 	OriginIDs          *lru.Cache | ||||
|  | ||||
| 	avatarAvailability map[string]bool | ||||
| 	avatarMap          map[string]string | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	stanzaIDs, _ := lru.New(5000) | ||||
| 	originIDs, _ := lru.New(5000) | ||||
| 	return &Bxmpp{ | ||||
| 		Config:             cfg, | ||||
| 		StanzaIDs:          stanzaIDs, | ||||
| 		OriginIDs:          originIDs, | ||||
| 		xmppMap:            make(map[string]string), | ||||
| 		avatarAvailability: make(map[string]bool), | ||||
| 		avatarMap:          make(map[string]string), | ||||
| @@ -123,13 +132,20 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	if msg.ParentNotFound() { | ||||
| 		msg.ParentID = "" | ||||
| 	} | ||||
|  | ||||
| 	// Post normal message. | ||||
| 	var msgReplaceID string | ||||
| 	msgID := xid.New().String() | ||||
| 	if msg.ID != "" { | ||||
| 		msgID = msg.ID | ||||
| 		msgReplaceID = msg.ID | ||||
| 	} | ||||
| 	var replyID string | ||||
| 	if res, ok := b.StanzaIDs.Get(msg.ParentID); ok { | ||||
| 		replyID, _ = res.(string) | ||||
| 	} | ||||
| 	b.Log.Debugf("=> Sending message %#v", msg) | ||||
| 	if _, err := b.xc.Send(xmpp.Chat{ | ||||
| 		Type:      "groupchat", | ||||
| @@ -137,6 +153,7 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { | ||||
| 		Text:      msg.Username + msg.Text, | ||||
| 		ID:        msgID, | ||||
| 		ReplaceID: msgReplaceID, | ||||
| 		ReplyID:   replyID, | ||||
| 	}); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| @@ -157,17 +174,32 @@ func (b *Bxmpp) postSlackCompatibleWebhook(msg config.Message) error { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	resp, err := http.Post(b.GetString("WebhookURL")+"/"+msg.Channel, "application/json", bytes.NewReader(webhookBody)) | ||||
| 	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 err | ||||
| 	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")) | ||||
| 	var serverName string | ||||
| 	switch { | ||||
| 	case !b.GetBool("Anonymous"): | ||||
| 		if !strings.Contains(b.GetString("Jid"), "@") { | ||||
| 			return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid")) | ||||
| 		} | ||||
| 		serverName = strings.Split(b.GetString("Jid"), "@")[1] | ||||
| 	case !strings.Contains(b.GetString("Server"), ":"): | ||||
| 		serverName = strings.Split(b.GetString("Server"), ":")[0] | ||||
| 	default: | ||||
| 		serverName = b.GetString("Server") | ||||
| 	} | ||||
|  | ||||
| 	tc := &tls.Config{ | ||||
| 		ServerName:         strings.Split(b.GetString("Jid"), "@")[1], | ||||
| 		ServerName:         serverName, | ||||
| 		InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec | ||||
| 	} | ||||
|  | ||||
| @@ -268,7 +300,13 @@ func (b *Bxmpp) handleXMPP() error { | ||||
| 	for { | ||||
| 		m, err := b.xc.Recv() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			// An error together with AvatarData is non-fatal | ||||
| 			switch m.(type) { | ||||
| 			case xmpp.AvatarData: | ||||
| 				continue | ||||
| 			default: | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		switch v := m.(type) { | ||||
| @@ -276,6 +314,11 @@ func (b *Bxmpp) handleXMPP() error { | ||||
| 			if v.Type == "groupchat" { | ||||
| 				b.Log.Debugf("== Receiving %#v", v) | ||||
|  | ||||
| 				if v.ID != "" && v.StanzaID != "" { | ||||
| 					b.StanzaIDs.Add(v.ID, v.StanzaID) | ||||
| 					b.OriginIDs.Add(v.StanzaID, v.ID) | ||||
| 				} | ||||
|  | ||||
| 				// Skip invalid messages. | ||||
| 				if b.skipMessage(v) { | ||||
| 					continue | ||||
| @@ -300,6 +343,12 @@ func (b *Bxmpp) handleXMPP() error { | ||||
| 				if v.ReplaceID != "" { | ||||
| 					msgID = v.ReplaceID | ||||
| 				} | ||||
|  | ||||
| 				var parentID string | ||||
| 				if res, ok := b.OriginIDs.Get(v.ReplyID); ok { | ||||
| 					parentID, _ = res.(string) | ||||
| 				} | ||||
|  | ||||
| 				rmsg := config.Message{ | ||||
| 					Username: b.parseNick(v.Remote), | ||||
| 					Text:     v.Text, | ||||
| @@ -307,6 +356,7 @@ func (b *Bxmpp) handleXMPP() error { | ||||
| 					Account:  b.Account, | ||||
| 					Avatar:   avatar, | ||||
| 					UserID:   v.Remote, | ||||
| 					ParentID: parentID, | ||||
| 					ID:       msgID, | ||||
| 					Event:    event, | ||||
| 				} | ||||
| @@ -379,7 +429,7 @@ func (b *Bxmpp) handleUploadFile(msg *config.Message) error { | ||||
|  | ||||
| func (b *Bxmpp) parseNick(remote string) string { | ||||
| 	s := strings.Split(remote, "@") | ||||
| 	if len(s) > 0 { | ||||
| 	if len(s) > 1 { | ||||
| 		s = strings.Split(s[1], "/") | ||||
| 		if len(s) == 2 { | ||||
| 			return s[1] // nick | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package bzulip | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| @@ -11,6 +12,7 @@ import ( | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/42wim/matterbridge/version" | ||||
| 	gzb "github.com/matterbridge/gozulipbot" | ||||
| ) | ||||
|  | ||||
| @@ -27,7 +29,7 @@ func New(cfg *bridge.Config) bridge.Bridger { | ||||
| } | ||||
|  | ||||
| func (b *Bzulip) Connect() error { | ||||
| 	bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login")} | ||||
| 	bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login"), UserAgent: fmt.Sprintf("matterbridge/%s", version.Release)} | ||||
| 	bot.Init() | ||||
| 	q, err := bot.RegisterAll() | ||||
| 	b.q = q | ||||
| @@ -104,29 +106,31 @@ func (b *Bzulip) getChannel(id int) string { | ||||
| func (b *Bzulip) handleQueue() error { | ||||
| 	for { | ||||
| 		messages, err := b.q.GetEvents() | ||||
| 		switch err { | ||||
| 		case gzb.BackoffError: | ||||
| 			time.Sleep(time.Second * 5) | ||||
| 		case gzb.NoJSONError: | ||||
| 			b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds") | ||||
| 			time.Sleep(time.Second * 10) | ||||
| 		case gzb.BadEventQueueError: | ||||
| 			b.Log.Info("got a bad event queue id error, reconnecting") | ||||
| 			b.bot.Queues = nil | ||||
| 			for { | ||||
| 				b.q, err = b.bot.RegisterAll() | ||||
| 				if err != nil { | ||||
| 					b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err) | ||||
| 					time.Sleep(time.Second * 10) | ||||
| 				} | ||||
| 				break | ||||
| 			} | ||||
| 		case gzb.HeartbeatError: | ||||
| 			b.Log.Debug("heartbeat received.") | ||||
| 		default: | ||||
| 			b.Log.Debugf("receiving error: %#v", err) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			switch err { | ||||
| 			case gzb.BackoffError: | ||||
| 				time.Sleep(time.Second * 5) | ||||
| 			case gzb.NoJSONError: | ||||
| 				b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds") | ||||
| 				time.Sleep(time.Second * 10) | ||||
| 			case gzb.BadEventQueueError: | ||||
| 				b.Log.Info("got a bad event queue id error, reconnecting") | ||||
| 				b.bot.Queues = nil | ||||
| 				for { | ||||
| 					b.q, err = b.bot.RegisterAll() | ||||
| 					if err != nil { | ||||
| 						b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err) | ||||
| 						time.Sleep(time.Second * 10) | ||||
| 					} | ||||
| 					break | ||||
| 				} | ||||
| 			case gzb.HeartbeatError: | ||||
| 				b.Log.Debug("heartbeat received.") | ||||
| 			default: | ||||
| 				b.Log.Debugf("receiving error: %#v", err) | ||||
| 				time.Sleep(time.Second * 10) | ||||
| 			} | ||||
|  | ||||
| 			continue | ||||
| 		} | ||||
| 		for _, m := range messages { | ||||
|   | ||||
							
								
								
									
										1471
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										15
									
								
								contrib/inmessage-logger.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| fmt := import("fmt") | ||||
| os := import("os") | ||||
| times := import("times") | ||||
|  | ||||
| if msgText != "" && msgUsername != "system" { | ||||
|     os.chdir("/var/www/matterbridge") | ||||
|     file := os.open_file("inmessage.log", os.o_append|os.o_wronly|os.o_create, 0644) | ||||
|     file.write_string(fmt.sprintf( | ||||
|         "[%s] <%s> %s\n", | ||||
|         times.time_format(times.now(), times.format_rfc1123), | ||||
|         msgUsername, | ||||
|         msgText | ||||
|     )) | ||||
|     file.close() | ||||
| } | ||||
							
								
								
									
										19
									
								
								contrib/matterbridge.openrc
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| #!/sbin/openrc-run | ||||
| # Copyright 2021-2022 Gentoo Authors | ||||
| # Distributed under the terms of the GNU General Public License v2 | ||||
|  | ||||
| command=/usr/bin/matterbridge | ||||
| command_args="-conf ${MATTERBRIDGE_CONF:-/etc/matterbridge/bridge.toml} ${MATTERBRIDGE_ARGS}" | ||||
| command_user="matterbridge:matterbridge" | ||||
| pidfile="/run/${RC_SVCNAME}.pid" | ||||
| command_background=1 | ||||
| output_log="/var/log/${RC_SVCNAME}.log" | ||||
| error_log="${output_log}" | ||||
|  | ||||
| depend() { | ||||
| 	need net | ||||
| } | ||||
|  | ||||
| start_pre() { | ||||
| 	checkpath -f "${output_log}" -o "${command_user}" || return 1 | ||||
| } | ||||
							
								
								
									
										6
									
								
								contrib/mumble-autolink.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| text := import("text") | ||||
|  | ||||
| if outProtocol == "mumble" { | ||||
|     urlRE := text.re_compile(`(?is)((http|https):\/\/)?([a-z0-9-]+\.)?[a-z0-9-]+(\.[a-z]{2,6}){1,3}(\/[a-z0-9.,_\/~#&=;%+?-]*)?`) | ||||
|     msgText = urlRE.replace(msgText,`<a href="$0">$0</a>`) | ||||
| } | ||||
| @@ -1,9 +1,10 @@ | ||||
| FROM alpine:edge as certs | ||||
| RUN apk --update add ca-certificates | ||||
| ARG VERSION=1.22.3 | ||||
| ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-${VERSION}-linux-arm64 /bin/matterbridge | ||||
| RUN chmod +x /bin/matterbridge | ||||
|  | ||||
| FROM scratch | ||||
| ARG VERSION=1.12.3 | ||||
| COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt | ||||
| ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-linux-arm /bin/matterbridge | ||||
| RUN chmod +x /bin/matterbridge | ||||
| COPY --from=certs /bin/matterbridge /bin/matterbridge | ||||
| ENTRYPOINT ["/bin/matterbridge"] | ||||
|   | ||||
| @@ -1,11 +0,0 @@ | ||||
| // +build !nogitter | ||||
|  | ||||
| package bridgemap | ||||
|  | ||||
| import ( | ||||
| 	bgitter "github.com/42wim/matterbridge/bridge/gitter" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	FullMap["gitter"] = bgitter.New | ||||
| } | ||||
							
								
								
									
										12
									
								
								gateway/bridgemap/bharmony.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| //go:build !noharmony | ||||
| // +build !noharmony | ||||
|  | ||||
| package bridgemap | ||||
|  | ||||
| import ( | ||||
| 	bharmony "github.com/42wim/matterbridge/bridge/harmony" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	FullMap["harmony"] = bharmony.New | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| // +build !nowhatsapp | ||||
| // +build !whatsappmulti | ||||
|  | ||||
| package bridgemap | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bwhatsappmulti.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| // +build whatsappmulti | ||||
|  | ||||
| package bridgemap | ||||
|  | ||||
| import ( | ||||
| 	bwhatsapp "github.com/42wim/matterbridge/bridge/whatsappmulti" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	FullMap["whatsapp"] = bwhatsapp.New | ||||
| } | ||||
| @@ -14,7 +14,7 @@ import ( | ||||
| 	"github.com/d5/tengo/v2" | ||||
| 	"github.com/d5/tengo/v2/stdlib" | ||||
| 	lru "github.com/hashicorp/golang-lru" | ||||
| 	"github.com/matterbridge/emoji" | ||||
| 	"github.com/kyokomi/emoji/v2" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| @@ -66,7 +66,7 @@ func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway { | ||||
| func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string { | ||||
| 	ID := protocol + " " + mID | ||||
| 	if gw.Messages.Contains(ID) { | ||||
| 		return mID | ||||
| 		return ID | ||||
| 	} | ||||
|  | ||||
| 	// If not keyed, iterate through cache for downstream, and infer upstream. | ||||
| @@ -75,7 +75,7 @@ func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string { | ||||
| 		ids := v.([]*BrMsgID) | ||||
| 		for _, downstreamMsgObj := range ids { | ||||
| 			if ID == downstreamMsgObj.ID { | ||||
| 				return strings.Replace(mid.(string), protocol+" ", "", 1) | ||||
| 				return mid.(string) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| @@ -127,7 +127,7 @@ func (gw *Gateway) AddConfig(cfg *config.Gateway) error { | ||||
| 		gw.logger.Errorf("mapChannels() failed: %s", err) | ||||
| 	} | ||||
| 	for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) { | ||||
| 		br := br //scopelint | ||||
| 		br := br // scopelint | ||||
| 		err := gw.AddBridge(&br) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| @@ -299,13 +299,30 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool { | ||||
|  | ||||
| 	igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks")) | ||||
| 	igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages")) | ||||
| 	if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) { | ||||
| 	if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) || gw.ignoreFilesComment(msg.Extra, igMessages) { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // ignoreFilesComment returns true if we need to ignore a file with matched comment. | ||||
| func (gw *Gateway) ignoreFilesComment(extra map[string][]interface{}, igMessages []string) bool { | ||||
| 	if extra == nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	for _, f := range extra["file"] { | ||||
| 		fi, ok := f.(config.FileInfo) | ||||
| 		if !ok { | ||||
| 			continue | ||||
| 		} | ||||
| 		if gw.ignoreText(fi.Comment, igMessages) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string { | ||||
| 	if dest.GetBool("StripNick") { | ||||
| 		re := regexp.MustCompile("[^a-zA-Z0-9]+") | ||||
| @@ -386,6 +403,7 @@ func (gw *Gateway) modifyMessage(msg *config.Message) { | ||||
| 	} | ||||
|  | ||||
| 	// replace :emoji: to unicode | ||||
| 	emoji.ReplacePadding = "" | ||||
| 	msg.Text = emoji.Sprint(msg.Text) | ||||
|  | ||||
| 	br := gw.Bridges[msg.Account] | ||||
| @@ -446,16 +464,19 @@ func (gw *Gateway) SendMessage( | ||||
| 	msg.Avatar = gw.modifyAvatar(rmsg, dest) | ||||
| 	msg.Username = gw.modifyUsername(rmsg, dest) | ||||
|  | ||||
| 	msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel) | ||||
| 	// exclude file delete event as the msg ID here is the native file ID that needs to be deleted | ||||
| 	if msg.Event != config.EventFileDelete { | ||||
| 		msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel) | ||||
| 	} | ||||
|  | ||||
| 	// for api we need originchannel as channel | ||||
| 	if dest.Protocol == apiProtocol { | ||||
| 		msg.Channel = rmsg.Channel | ||||
| 	} | ||||
|  | ||||
| 	msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel) | ||||
| 	msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel) | ||||
| 	if msg.ParentID == "" { | ||||
| 		msg.ParentID = canonicalParentMsgID | ||||
| 		msg.ParentID = strings.Replace(canonicalParentMsgID, dest.Protocol+" ", "", 1) | ||||
| 	} | ||||
|  | ||||
| 	// if the parentID is still empty and we have a parentID set in the original message | ||||
| @@ -496,7 +517,7 @@ func (gw *Gateway) SendMessage( | ||||
| 	if mID != "" { | ||||
| 		gw.logger.Debugf("mID %s: %s", dest.Account, mID) | ||||
| 		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 | ||||
| } | ||||
|   | ||||
| @@ -18,8 +18,6 @@ var testconfig = []byte(` | ||||
| server="" | ||||
| [mattermost.test] | ||||
| server="" | ||||
| [gitter.42wim] | ||||
| server="" | ||||
| [discord.test] | ||||
| server="" | ||||
| [slack.test] | ||||
| @@ -33,11 +31,6 @@ server="" | ||||
|     account = "irc.freenode" | ||||
|     channel = "#wimtesting" | ||||
|      | ||||
|     [[gateway.inout]] | ||||
|     account="gitter.42wim" | ||||
|     channel="42wim/testroom" | ||||
|     #channel="matterbridge/Lobby" | ||||
|  | ||||
|     [[gateway.inout]] | ||||
|     account = "discord.test" | ||||
|     channel = "general" | ||||
| @@ -52,8 +45,6 @@ var testconfig2 = []byte(` | ||||
| server="" | ||||
| [mattermost.test] | ||||
| server="" | ||||
| [gitter.42wim] | ||||
| server="" | ||||
| [discord.test] | ||||
| server="" | ||||
| [slack.test] | ||||
| @@ -67,10 +58,6 @@ server="" | ||||
|     account = "irc.freenode" | ||||
|     channel = "#wimtesting" | ||||
|      | ||||
|     [[gateway.in]] | ||||
|     account="gitter.42wim" | ||||
|     channel="42wim/testroom" | ||||
|  | ||||
|     [[gateway.inout]] | ||||
|     account = "discord.test" | ||||
|     channel = "general" | ||||
| @@ -86,10 +73,6 @@ server="" | ||||
|     account = "irc.freenode" | ||||
|     channel = "#wimtesting2" | ||||
|      | ||||
|     [[gateway.out]] | ||||
|     account="gitter.42wim" | ||||
|     channel="42wim/testroom" | ||||
|  | ||||
|     [[gateway.out]] | ||||
|     account = "discord.test" | ||||
|     channel = "general2" | ||||
| @@ -184,31 +167,18 @@ func maketestRouter(input []byte) *Router { | ||||
| 	} | ||||
| 	return r | ||||
| } | ||||
|  | ||||
| func TestNewRouter(t *testing.T) { | ||||
| 	r := maketestRouter(testconfig) | ||||
| 	assert.Equal(t, 1, len(r.Gateways)) | ||||
| 	assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges)) | ||||
| 	assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels)) | ||||
| 	assert.Equal(t, 3, len(r.Gateways["bridge1"].Bridges)) | ||||
| 	assert.Equal(t, 3, len(r.Gateways["bridge1"].Channels)) | ||||
| 	r = maketestRouter(testconfig2) | ||||
| 	assert.Equal(t, 2, len(r.Gateways)) | ||||
| 	assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges)) | ||||
| 	assert.Equal(t, 3, len(r.Gateways["bridge2"].Bridges)) | ||||
| 	assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels)) | ||||
| 	assert.Equal(t, 3, len(r.Gateways["bridge2"].Channels)) | ||||
| 	assert.Equal(t, &config.ChannelInfo{ | ||||
| 		Name:        "42wim/testroom", | ||||
| 		Direction:   "out", | ||||
| 		ID:          "42wim/testroomgitter.42wim", | ||||
| 		Account:     "gitter.42wim", | ||||
| 		SameChannel: map[string]bool{"bridge2": false}, | ||||
| 	}, r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"]) | ||||
| 	assert.Equal(t, &config.ChannelInfo{ | ||||
| 		Name:        "42wim/testroom", | ||||
| 		Direction:   "in", | ||||
| 		ID:          "42wim/testroomgitter.42wim", | ||||
| 		Account:     "gitter.42wim", | ||||
| 		SameChannel: map[string]bool{"bridge1": false}, | ||||
| 	}, r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"]) | ||||
| 	assert.Equal(t, 3, len(r.Gateways["bridge1"].Bridges)) | ||||
| 	assert.Equal(t, 2, len(r.Gateways["bridge2"].Bridges)) | ||||
| 	assert.Equal(t, 3, len(r.Gateways["bridge1"].Channels)) | ||||
| 	assert.Equal(t, 2, len(r.Gateways["bridge2"].Channels)) | ||||
| 	assert.Equal(t, &config.ChannelInfo{ | ||||
| 		Name:        "general", | ||||
| 		Direction:   "inout", | ||||
| @@ -241,8 +211,6 @@ func TestGetDestChannel(t *testing.T) { | ||||
| 				SameChannel: map[string]bool{"bridge1": false}, | ||||
| 				Options:     config.ChannelOptions{Key: ""}, | ||||
| 			}}, r.Gateways["bridge1"].getDestChannel(msg, *br)) | ||||
| 		case "gitter.42wim": | ||||
| 			assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br)) | ||||
| 		case "irc.freenode": | ||||
| 			assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br)) | ||||
| 		} | ||||
| @@ -420,6 +388,7 @@ func (s *ignoreTestSuite) SetupSuite() { | ||||
| 	logger.SetOutput(ioutil.Discard) | ||||
| 	s.gw = &Gateway{logger: logrus.NewEntry(logger)} | ||||
| } | ||||
|  | ||||
| func (s *ignoreTestSuite) TestIgnoreTextEmpty() { | ||||
| 	extraFile := make(map[string][]interface{}) | ||||
| 	extraAttach := make(map[string][]interface{}) | ||||
| @@ -461,7 +430,6 @@ func (s *ignoreTestSuite) TestIgnoreTextEmpty() { | ||||
| 		output := s.gw.ignoreTextEmpty(testcase.input) | ||||
| 		s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname) | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func (s *ignoreTestSuite) TestIgnoreTexts() { | ||||
|   | ||||
| @@ -110,7 +110,9 @@ func (r *Router) disableBridge(br *bridge.Bridge, err error) bool { | ||||
| 	if r.BridgeValues().General.IgnoreFailureOnStart { | ||||
| 		r.logger.Error(err) | ||||
| 		// setting this bridge empty | ||||
| 		*br = bridge.Bridge{} | ||||
| 		*br = bridge.Bridge{ | ||||
| 			Log: br.Log, | ||||
| 		} | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
|   | ||||
							
								
								
									
										185
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						| @@ -1,62 +1,153 @@ | ||||
| module github.com/42wim/matterbridge | ||||
|  | ||||
| require ( | ||||
| 	github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 | ||||
| 	github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f | ||||
| 	github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f | ||||
| 	github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560 | ||||
| 	github.com/Rhymen/go-whatsapp v0.1.2-0.20210126174449-3c094ebae0ce | ||||
| 	github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20200922220614-e4a51dfb52e4 // indirect | ||||
| 	github.com/SevereCloud/vksdk/v2 v2.9.0 | ||||
| 	github.com/d5/tengo/v2 v2.6.2 | ||||
| 	github.com/davecgh/go-spew v1.1.1 | ||||
| 	github.com/fsnotify/fsnotify v1.4.9 | ||||
| 	github.com/go-telegram-bot-api/telegram-bot-api v1.0.1-0.20200524105306-7434b0456e81 | ||||
| 	github.com/gomarkdown/markdown v0.0.0-20201113031856-722100d81a8e | ||||
| 	github.com/google/gops v0.3.14 | ||||
| 	github.com/gorilla/schema v1.2.0 | ||||
| 	github.com/gorilla/websocket v1.4.2 | ||||
| 	github.com/hashicorp/golang-lru v0.5.4 | ||||
| 	github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c | ||||
| 	github.com/SevereCloud/vksdk/v2 v2.17.0 | ||||
| 	github.com/bwmarrin/discordgo v0.28.1 | ||||
| 	github.com/d5/tengo/v2 v2.17.0 | ||||
| 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc | ||||
| 	github.com/fsnotify/fsnotify v1.7.0 | ||||
| 	github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 | ||||
| 	github.com/google/gops v0.3.27 | ||||
| 	github.com/gorilla/schema v1.4.1 | ||||
| 	github.com/harmony-development/shibshib v0.0.0-20220101224523-c98059d09cfa | ||||
| 	github.com/hashicorp/golang-lru v1.0.2 | ||||
| 	github.com/jpillora/backoff v1.0.0 | ||||
| 	github.com/keybase/go-keybase-chat-bot v0.0.0-20200505163032-5cacf52379da | ||||
| 	github.com/labstack/echo/v4 v4.1.17 | ||||
| 	github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 | ||||
| 	github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd | ||||
| 	github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20201206215757-c1d86d75b9f8 | ||||
| 	github.com/matterbridge/discordgo v0.21.2-0.20210201201054-fb39a175b4f7 | ||||
| 	github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible | ||||
| 	github.com/matterbridge/go-xmpp v0.0.0-20200418225040-c8a3a57b4050 | ||||
| 	github.com/matterbridge/gozulipbot v0.0.0-20200820220548-be5824faa913 | ||||
| 	github.com/keybase/go-keybase-chat-bot v0.0.0-20221220212439-e48d9abd2c20 | ||||
| 	github.com/kyokomi/emoji/v2 v2.2.13 | ||||
| 	github.com/labstack/echo/v4 v4.12.0 | ||||
| 	github.com/lrstanley/girc v0.0.0-20240823210506-80555f2adb03 | ||||
| 	github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696 | ||||
| 	github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8 | ||||
| 	github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27 | ||||
| 	github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75 | ||||
| 	github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba | ||||
| 	github.com/mattermost/mattermost-server/v5 v5.30.1 | ||||
| 	github.com/matterbridge/matterclient v0.0.0-20240817214420-3d4c3aef3dc1 | ||||
| 	github.com/matterbridge/telegram-bot-api/v6 v6.5.0 | ||||
| 	github.com/mattermost/mattermost/server/public v0.1.6 | ||||
| 	github.com/mattn/godown v0.0.1 | ||||
| 	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect | ||||
| 	github.com/missdeer/golib v1.0.4 | ||||
| 	github.com/mitchellh/mapstructure v1.3.3 // indirect | ||||
| 	github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect | ||||
| 	github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect | ||||
| 	github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 | ||||
| 	github.com/mdp/qrterminal v1.0.1 | ||||
| 	github.com/mitchellh/mapstructure v1.5.0 | ||||
| 	github.com/nelsonken/gomf v0.0.0-20190423072027-c65cc0469e94 | ||||
| 	github.com/olahol/melody v1.2.1 | ||||
| 	github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c | ||||
| 	github.com/rs/xid v1.2.1 | ||||
| 	github.com/rs/xid v1.5.0 | ||||
| 	github.com/russross/blackfriday v1.6.0 | ||||
| 	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca | ||||
| 	github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d | ||||
| 	github.com/shazow/ssh-chat v1.10.1 | ||||
| 	github.com/sirupsen/logrus v1.7.0 | ||||
| 	github.com/slack-go/slack v0.8.0 | ||||
| 	github.com/spf13/afero v1.3.4 // indirect | ||||
| 	github.com/spf13/cast v1.3.1 // indirect | ||||
| 	github.com/spf13/jwalterweatherman v1.1.0 // indirect | ||||
| 	github.com/spf13/viper v1.7.1 | ||||
| 	github.com/stretchr/testify v1.7.0 | ||||
| 	github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 | ||||
| 	github.com/sirupsen/logrus v1.9.3 | ||||
| 	github.com/slack-go/slack v0.14.0 | ||||
| 	github.com/spf13/viper v1.19.0 | ||||
| 	github.com/stretchr/testify v1.9.0 | ||||
| 	github.com/vincent-petithory/dataurl v1.0.0 | ||||
| 	github.com/writeas/go-strip-markdown v2.0.1+incompatible | ||||
| 	github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect | ||||
| 	github.com/yaegashi/msgraph.go v0.1.4 | ||||
| 	github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2 | ||||
| 	golang.org/x/image v0.0.0-20201208152932-35266b937fa6 | ||||
| 	golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c | ||||
| 	gomod.garykim.dev/nc-talk v0.1.7 | ||||
| 	gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 | ||||
| 	layeh.com/gumble v0.0.0-20200818122324-146f9205029b | ||||
| 	github.com/zfjagann/golang-ring v0.0.0-20220330170733-19bcea1b6289 | ||||
| 	go.mau.fi/whatsmeow v0.0.0-20240821142752-3d63c6fcc1a7 | ||||
| 	golang.org/x/image v0.19.0 | ||||
| 	golang.org/x/oauth2 v0.22.0 | ||||
| 	golang.org/x/text v0.17.0 | ||||
| 	gomod.garykim.dev/nc-talk v0.3.0 | ||||
| 	google.golang.org/protobuf v1.34.2 | ||||
| 	layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14 | ||||
| 	modernc.org/sqlite v1.32.0 | ||||
| ) | ||||
|  | ||||
| go 1.15 | ||||
| require ( | ||||
| 	filippo.io/edwards25519 v1.1.0 // indirect | ||||
| 	github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989 // indirect | ||||
| 	github.com/Jeffail/gabs v1.4.0 // indirect | ||||
| 	github.com/apex/log v1.9.0 // indirect | ||||
| 	github.com/av-elier/go-decimal-to-rational v0.0.0-20191127152832-89e6aad02ecf // indirect | ||||
| 	github.com/blang/semver/v4 v4.0.0 // indirect | ||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||
| 	github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect | ||||
| 	github.com/fatih/color v1.17.0 // indirect | ||||
| 	github.com/francoispqt/gojay v1.2.13 // indirect | ||||
| 	github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect | ||||
| 	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect | ||||
| 	github.com/golang/protobuf v1.5.4 // indirect | ||||
| 	github.com/google/uuid v1.6.0 // indirect | ||||
| 	github.com/gopackage/ddp v0.0.3 // indirect | ||||
| 	github.com/gorilla/websocket v1.5.3 // indirect | ||||
| 	github.com/hashicorp/errwrap v1.1.0 // indirect | ||||
| 	github.com/hashicorp/go-hclog v1.6.3 // indirect | ||||
| 	github.com/hashicorp/go-multierror v1.1.1 // indirect | ||||
| 	github.com/hashicorp/go-plugin v1.6.1 // indirect | ||||
| 	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect | ||||
| 	github.com/hashicorp/hcl v1.0.0 // indirect | ||||
| 	github.com/hashicorp/yamux v0.1.1 // indirect | ||||
| 	github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect | ||||
| 	github.com/klauspost/compress v1.17.9 // indirect | ||||
| 	github.com/labstack/gommon v0.4.2 // indirect | ||||
| 	github.com/magiconair/properties v1.8.7 // indirect | ||||
| 	github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect | ||||
| 	github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect | ||||
| 	github.com/mattermost/logr/v2 v2.0.21 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||
| 	github.com/mattn/go-runewidth v0.0.15 // indirect | ||||
| 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect | ||||
| 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect | ||||
| 	github.com/monaco-io/request v1.0.5 // indirect | ||||
| 	github.com/ncruces/go-strftime v0.1.9 // indirect | ||||
| 	github.com/nxadm/tail v1.4.11 // indirect | ||||
| 	github.com/oklog/run v1.1.0 // indirect | ||||
| 	github.com/pborman/uuid v1.2.1 // indirect | ||||
| 	github.com/pelletier/go-toml v1.9.5 // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect | ||||
| 	github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect | ||||
| 	github.com/pkg/errors v0.9.1 // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | ||||
| 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect | ||||
| 	github.com/rickb777/date v1.12.4 // indirect | ||||
| 	github.com/rickb777/plural v1.2.0 // indirect | ||||
| 	github.com/rivo/uniseg v0.4.7 // indirect | ||||
| 	github.com/rs/zerolog v1.33.0 // indirect | ||||
| 	github.com/sagikazarmark/locafero v0.4.0 // indirect | ||||
| 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect | ||||
| 	github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 // indirect | ||||
| 	github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 // indirect | ||||
| 	github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 // indirect | ||||
| 	github.com/sourcegraph/conc v0.3.0 // indirect | ||||
| 	github.com/spf13/afero v1.11.0 // indirect | ||||
| 	github.com/spf13/cast v1.6.0 // indirect | ||||
| 	github.com/spf13/pflag v1.0.5 // indirect | ||||
| 	github.com/subosito/gotenv v1.6.0 // indirect | ||||
| 	github.com/tinylib/msgp v1.2.0 // indirect | ||||
| 	github.com/valyala/bytebufferpool v1.0.0 // indirect | ||||
| 	github.com/valyala/fasttemplate v1.2.2 // indirect | ||||
| 	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect | ||||
| 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect | ||||
| 	github.com/wiggin77/merror v1.0.5 // indirect | ||||
| 	github.com/wiggin77/srslog v1.0.1 // indirect | ||||
| 	go.mau.fi/libsignal v0.1.1 // indirect | ||||
| 	go.mau.fi/util v0.6.0 // indirect | ||||
| 	go.uber.org/multierr v1.11.0 // indirect | ||||
| 	golang.org/x/crypto v0.25.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect | ||||
| 	golang.org/x/net v0.27.0 // indirect | ||||
| 	golang.org/x/sys v0.22.0 // indirect | ||||
| 	golang.org/x/term v0.22.0 // indirect | ||||
| 	golang.org/x/time v0.5.0 // indirect | ||||
| 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect | ||||
| 	google.golang.org/grpc v1.65.0 // indirect | ||||
| 	gopkg.in/ini.v1 v1.67.0 // indirect | ||||
| 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | ||||
| 	gopkg.in/yaml.v2 v2.4.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| 	modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect | ||||
| 	modernc.org/libc v1.55.3 // indirect | ||||
| 	modernc.org/mathutil v1.6.0 // indirect | ||||
| 	modernc.org/memory v1.8.0 // indirect | ||||
| 	modernc.org/strutil v1.2.0 // indirect | ||||
| 	modernc.org/token v1.1.0 // indirect | ||||
| 	rsc.io/qr v0.2.0 // indirect | ||||
| ) | ||||
|  | ||||
| //replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419 | ||||
|  | ||||
| go 1.22.0 | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import ( | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"regexp" | ||||
| ) | ||||
|  | ||||
| // Message for rocketchat outgoing webhook. | ||||
| @@ -68,7 +69,6 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	} | ||||
| 	msg := Message{} | ||||
| 	body, err := ioutil.ReadAll(r.Body) | ||||
| 	log.Println(string(body)) | ||||
| 	if err != nil { | ||||
| 		log.Println(err) | ||||
| 		http.NotFound(w, r) | ||||
| @@ -89,7 +89,11 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	msg.ChannelName = "#" + msg.ChannelName | ||||
| 	if c.Token != "" { | ||||
| 		if msg.Token != c.Token { | ||||
| 			log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr) | ||||
| 			if regexp.MustCompile(`[^a-zA-Z0-9]+`).MatchString(msg.Token) { | ||||
| 				log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr) | ||||
| 			} else { | ||||
| 				log.Println("invalid token from " + r.RemoteAddr) | ||||
| 			} | ||||
| 			http.NotFound(w, r) | ||||
| 			return | ||||
| 		} | ||||
|   | ||||
| Before Width: | Height: | Size: 270 KiB | 
| Before Width: | Height: | Size: 170 KiB | 
| Before Width: | Height: | Size: 282 KiB | 
| Before Width: | Height: | Size: 204 KiB | 
| Before Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 73 KiB | 
| Before Width: | Height: | Size: 62 KiB | 
| @@ -10,15 +10,13 @@ import ( | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/gateway" | ||||
| 	"github.com/42wim/matterbridge/gateway/bridgemap" | ||||
| 	"github.com/42wim/matterbridge/version" | ||||
| 	"github.com/google/gops/agent" | ||||
| 	prefixed "github.com/matterbridge/logrus-prefixed-formatter" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	version = "1.22.0" | ||||
| 	githash string | ||||
|  | ||||
| 	flagConfig  = flag.String("conf", "matterbridge.toml", "config file") | ||||
| 	flagDebug   = flag.Bool("debug", false, "enable debug") | ||||
| 	flagVersion = flag.Bool("version", false, "show version") | ||||
| @@ -28,7 +26,7 @@ var ( | ||||
| func main() { | ||||
| 	flag.Parse() | ||||
| 	if *flagVersion { | ||||
| 		fmt.Printf("version: %s %s\n", version, githash) | ||||
| 		fmt.Printf("version: %s %s\n", version.Release, version.GitHash) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -43,8 +41,8 @@ func main() { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	logger.Printf("Running version %s %s", version, githash) | ||||
| 	if strings.Contains(version, "-dev") { | ||||
| 	logger.Printf("Running version %s %s", version.Release, version.GitHash) | ||||
| 	if strings.Contains(version.Release, "-dev") { | ||||
| 		logger.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.") | ||||
| 	} | ||||
|  | ||||
| @@ -67,7 +65,7 @@ func main() { | ||||
| 	if err = r.Start(); err != nil { | ||||
| 		logger.Fatalf("Starting gateway failed: %s", err) | ||||
| 	} | ||||
| 	logger.Printf("Gateway(s) started succesfully. Now relaying messages") | ||||
| 	logger.Printf("Gateway(s) started successfully. Now relaying messages") | ||||
| 	select {} | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										61
									
								
								matterbridge.toml.multi
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,61 @@ | ||||
| #WARNING: as this file contains credentials, be sure to set correct file permissions | ||||
|  | ||||
| [irc] | ||||
|     [irc.foo] | ||||
|     Server="irc.myfooserver.com:6667" | ||||
|     Nick="matterbot" | ||||
|  | ||||
| # Can also connect to multiple different servers of the same protocol: | ||||
| [irc] | ||||
|     [irc.bar] | ||||
|     Server="irc.mybarserver.com:6667" | ||||
|     Nick="matterbot" | ||||
|  | ||||
| [telegram] | ||||
|     [telegram.mytelegram] | ||||
|     Token="123456789:FillInYourTokenHereThatIsImportant" | ||||
|  | ||||
| [mattermost] | ||||
|     [mattermost.work] | ||||
|     #do not prefix it wit http:// or https:// | ||||
|     Server="yourmattermostserver.domain"  | ||||
|     Team="yourteam" | ||||
|     Login="yourlogin" | ||||
|     Password="yourpass" | ||||
|     PrefixMessagesWithNick=true | ||||
|  | ||||
| # Bridge 1: Copy all messages from all rooms to all rooms. | ||||
| # This shows how you can have multiple rooms in a single bridge. | ||||
| [[gateway]] | ||||
| name="cats-are-cool" | ||||
| enable=true | ||||
|     [[gateway.inout]] | ||||
|     account="irc.foo" | ||||
|     channel="#cats-are-cool" | ||||
|     [[gateway.inout]] | ||||
|     account="irc.bar" | ||||
|     channel="#cats-are-cool" | ||||
|     [[gateway.inout]] | ||||
|     account="telegram.mytelegram" | ||||
|     channel="-1234567890123" | ||||
|     [[gateway.inout]] | ||||
|     account="mattermost.work" | ||||
|     channel="cats-are-cool" | ||||
|  | ||||
| # Bridge 2: Copy some messages from some rooms to some rooms. | ||||
| # This shows how you can have multiple bridges. | ||||
| [[gateway]] | ||||
| name="dog-announcements" | ||||
| enable=true | ||||
|     [[gateway.in]] | ||||
|     account="irc.foo" | ||||
|     channel="#dog-announcements" | ||||
|     [[gateway.in]] | ||||
|     account="irc.bar" | ||||
|     channel="#dog-announcements" | ||||
|     [[gateway.out]] | ||||
|     account="telegram.mytelegram" | ||||
|     channel="-9876543219876" | ||||
|     [[gateway.out]] | ||||
|     account="mattermost.work" | ||||
|     channel="dog-announcements" | ||||
| @@ -9,12 +9,12 @@ | ||||
| [irc] | ||||
|  | ||||
| #You can configure multiple servers "[irc.name]" or "[irc.name2]" | ||||
| #In this example we use [irc.freenode] | ||||
| #In this example we use [irc.libera] | ||||
| #REQUIRED | ||||
| [irc.freenode] | ||||
| [irc.libera] | ||||
| #irc server to connect to. | ||||
| #REQUIRED | ||||
| Server="irc.freenode.net:6667" | ||||
| Server="irc.libera.chat:6667" | ||||
|  | ||||
| #Password for irc server (if necessary) | ||||
| #OPTIONAL (default "") | ||||
| @@ -24,7 +24,14 @@ Password="" | ||||
| #OPTIONAL (default false) | ||||
| UseTLS=false | ||||
|  | ||||
| #Enable SASL (PLAIN) authentication. (freenode requires this from eg AWS hosts) | ||||
| #Use client certificate - see CertFP https://libera.chat/guides/certfp.html | ||||
| #Specify filename which contains private key and cert | ||||
| #OPTIONAL (default "") | ||||
| # | ||||
| #TLSClientCertificate="cert.pem" | ||||
| TLSClientCertificate="" | ||||
|  | ||||
| #Enable SASL (PLAIN) authentication. (libera requires this from eg AWS hosts) | ||||
| #It uses NickServNick and NickServPassword as login and password | ||||
| #OPTIONAL (default false) | ||||
| UseSASL=false | ||||
| @@ -34,6 +41,11 @@ UseSASL=false | ||||
| #OPTIONAL (default false) | ||||
| SkipTLSVerify=true | ||||
|  | ||||
| #Local address to use for server connection | ||||
| #Note that Server and Bind must resolve to addresses of the same family. | ||||
| #OPTIONAL (default "") | ||||
| Bind="" | ||||
|  | ||||
| #If you know your charset, you can specify it manually. | ||||
| #Otherwise it tries to detect this automatically. Select one below | ||||
| # "iso-8859-2:1987", "iso-8859-9:1989", "866", "latin9", "iso-8859-10:1992", "iso-ir-109", "hebrew", | ||||
| @@ -55,7 +67,15 @@ Charset="" | ||||
| #REQUIRED | ||||
| Nick="matterbot" | ||||
|  | ||||
| #If you registered your bot with a service like Nickserv on freenode. | ||||
| #Real name/gecos displayed in e.g. /WHOIS and /WHO | ||||
| #OPTIONAL (defaults to the nick) | ||||
| RealName="Matterbridge instance on IRC" | ||||
|  | ||||
| #IRC username/ident preceding the hostname in hostmasks and /WHOIS | ||||
| #OPTIONAL (defaults to the nick) | ||||
| UserName="bridge" | ||||
|  | ||||
| #If you registered your bot with a service like Nickserv on libera. | ||||
| #Also being used when UseSASL=true | ||||
| # | ||||
| #Note: if you want do to quakenet auth, set NickServNick="Q@CServe.quakenet.org" | ||||
| @@ -76,20 +96,24 @@ MessageDelay=1300 | ||||
|  | ||||
| #Maximum amount of messages to hold in queue. If queue is full | ||||
| #messages will be dropped. | ||||
| #<message clipped> will be add to the message that fills the queue. | ||||
| #<clipped message> will be add to the message that fills the queue. | ||||
| #OPTIONAL (default 30) | ||||
| MessageQueue=30 | ||||
|  | ||||
| #Maximum length of message sent to irc server. If it exceeds | ||||
| #<message clipped> will be add to the message. | ||||
| #<clipped message> will be add to the message. | ||||
| #OPTIONAL (default 400) | ||||
| MessageLength=400 | ||||
|  | ||||
| #Split messages on MessageLength instead of showing the <message clipped> | ||||
| #Split messages on MessageLength instead of showing the <clipped message> | ||||
| #WARNING: this could lead to flooding | ||||
| #OPTIONAL (default false) | ||||
| MessageSplit=false | ||||
|  | ||||
| #Message to show when a message is too big | ||||
| #Default "<clipped message>" | ||||
| MessageClipped="<clipped message>" | ||||
|  | ||||
| #Delay in seconds to rejoin a channel when kicked | ||||
| #OPTIONAL (default 0) | ||||
| RejoinDelay=0 | ||||
| @@ -98,10 +122,11 @@ RejoinDelay=0 | ||||
| #Only works in IRC right now. | ||||
| ColorNicks=false | ||||
|  | ||||
| #RunCommands allows you to send RAW irc commands after connection | ||||
| #RunCommands allows you to send RAW irc commands after connection. | ||||
| #The string {BOTNICK} (case sensitive) will be replaced with the bot's current nickname. | ||||
| #Array of strings | ||||
| #OPTIONAL (default empty) | ||||
| RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"] | ||||
| RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something", "MODE {BOTNICK} +B"] | ||||
|  | ||||
| #PingDelay specifies how long to wait to send a ping to the irc server. | ||||
| #You can use s for second, m for minute | ||||
| @@ -138,7 +163,7 @@ IgnoreMessages="^~~ badword" | ||||
| ReplaceMessages=[ ["cat","dog"] ] | ||||
|  | ||||
| #nicks you want to replace. | ||||
| #see replacemessages for syntaxa | ||||
| #see replacemessages for syntax | ||||
| #optional (default empty) | ||||
| ReplaceNicks=[ ["user--","user"] ] | ||||
|  | ||||
| @@ -163,7 +188,7 @@ Label="" | ||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| @@ -173,7 +198,7 @@ ShowJoinPart=false | ||||
| VerboseJoinPart=false | ||||
|  | ||||
| #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, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| NoSendJoinPart=false | ||||
|  | ||||
| @@ -219,12 +244,16 @@ UseRelayMsg=false | ||||
| #REQUIRED | ||||
| Server="jabber.example.com:5222" | ||||
|  | ||||
| #Use anonymous MUC login | ||||
| #OPTIONAL (default false) | ||||
| Anonymous=false | ||||
|  | ||||
| #Jid | ||||
| #REQUIRED | ||||
| #REQUIRED if Anonymous=false | ||||
| Jid="user@example.com" | ||||
|  | ||||
| #Password | ||||
| #REQUIRED | ||||
| #REQUIRED if Anonymous=false | ||||
| Password="yourpass" | ||||
|  | ||||
| #MUC | ||||
| @@ -272,7 +301,7 @@ IgnoreMessages="^~~ badword" | ||||
| ReplaceMessages=[ ["cat","dog"] ] | ||||
|  | ||||
| #Nicks you want to replace. | ||||
| #See ReplaceMessages for syntaxA | ||||
| #See ReplaceMessages for syntax | ||||
| #OPTIONAL (default empty) | ||||
| ReplaceNicks=[ ["user--","user"] ] | ||||
|  | ||||
| @@ -296,7 +325,7 @@ Label="" | ||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| @@ -380,6 +409,10 @@ SkipTLSVerify=true | ||||
| ## RELOADABLE SETTINGS | ||||
| ## Settings below can be reloaded by editing the file | ||||
|  | ||||
| # UseUserName shows the username instead of the server nickname | ||||
| # OPTIONAL (default false) | ||||
| UseUserName=false | ||||
|  | ||||
| #how to format the list of IRC nicks when displayed in mattermost. | ||||
| #Possible options are "table" and "plain" | ||||
| #OPTIONAL (default plain) | ||||
| @@ -435,7 +468,7 @@ IgnoreMessages="^~~ badword" | ||||
| ReplaceMessages=[ ["cat","dog"] ] | ||||
|  | ||||
| #nicks you want to replace. | ||||
| #see replacemessages for syntaxa | ||||
| #see replacemessages for syntax | ||||
| #optional (default empty) | ||||
| ReplaceNicks=[ ["user--","user"] ] | ||||
|  | ||||
| @@ -459,12 +492,12 @@ Label="" | ||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| #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, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| NoSendJoinPart=false | ||||
|  | ||||
| @@ -480,86 +513,9 @@ ShowTopicChange=false | ||||
|  | ||||
| ################################################################### | ||||
| #Gitter section | ||||
| #Best to make a dedicated gitter account for the bot. | ||||
| #Gitter has been moved to matrix - see https://github.com/42wim/matterbridge/issues/1969 how to migrate | ||||
| ################################################################### | ||||
|  | ||||
| [gitter] | ||||
|  | ||||
| #You can configure multiple servers "[gitter.name]" or "[gitter.name2]" | ||||
| #In this example we use [gitter.myproject] | ||||
| #REQUIRED | ||||
| [gitter.myproject] | ||||
| #Token to connect with Gitter API | ||||
| #You can get your token by going to https://developer.gitter.im/docs/welcome and SIGN IN | ||||
| #REQUIRED | ||||
| Token="Yourtokenhere" | ||||
|  | ||||
| ## 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 | ||||
|  | ||||
| ################################################################### | ||||
| # | ||||
| # Keybase | ||||
| @@ -630,7 +586,7 @@ IgnoreMessages="^~~ badword" | ||||
| ReplaceMessages=[ ["cat","dog"] ] | ||||
|  | ||||
| #nicks you want to replace. | ||||
| #see replacemessages for syntaxa | ||||
| #see replacemessages for syntax | ||||
| #optional (default empty) | ||||
| ReplaceNicks=[ ["user--","user"] ] | ||||
|  | ||||
| @@ -654,7 +610,7 @@ Label="" | ||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| @@ -769,7 +725,7 @@ IgnoreMessages="^~~ badword" | ||||
| ReplaceMessages=[ ["cat","dog"] ] | ||||
|  | ||||
| #nicks you want to replace. | ||||
| #see replacemessages for syntaxa | ||||
| #see replacemessages for syntax | ||||
| #optional (default empty) | ||||
| ReplaceNicks=[ ["user--","user"] ] | ||||
|  | ||||
| @@ -793,12 +749,12 @@ Label="" | ||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| #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, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| NoSendJoinPart=false | ||||
|  | ||||
| @@ -826,6 +782,14 @@ PreserveThreading=false | ||||
| #OPTIONAL (default false) | ||||
| ShowUserTyping=false | ||||
|  | ||||
| #Message to show when a message is too big | ||||
| #Default "<clipped message>" | ||||
| MessageClipped="<clipped message>" | ||||
|  | ||||
| #If enabled use the slack "Real Name" as username. | ||||
| #OPTIONAL (default false) | ||||
| UseFullName=false | ||||
|  | ||||
| ################################################################### | ||||
| #discord section | ||||
| ################################################################### | ||||
| @@ -848,6 +812,14 @@ Server="yourservername" | ||||
| ## All settings below can be reloaded by editing the file. | ||||
| ## They are also all optional. | ||||
|  | ||||
| # AllowMention controls which mentions are allowed. If not specified, all mentions are allowed. | ||||
| # Note that even when a mention is not allowed, it will still be displayed nicely and be clickable. It just prevents the ping/notification. | ||||
| # | ||||
| # "everyone" allows @everyone and @here mentions | ||||
| # "roles" allows @role mentions | ||||
| # "users" allows @user mentions | ||||
| AllowMention=["everyone", "roles", "users"] | ||||
|  | ||||
| # ShowEmbeds shows the title, description and URL of embedded messages (sent by other bots) | ||||
| ShowEmbeds=false | ||||
|  | ||||
| @@ -953,6 +925,17 @@ ShowTopicChange=false | ||||
| # Supported from the following bridges: slack | ||||
| SyncTopic=false | ||||
|  | ||||
| # Message to show when a message is too big | ||||
| # Default "<clipped message>" | ||||
| MessageClipped="<clipped message>" | ||||
|  | ||||
| # Before clipping, try to split messages into at most this many parts. 0 is treated like 1. | ||||
| # Be careful with large numbers, as this might cause flooding. | ||||
| # Example: A maximum telegram message of 4096 bytes is received. This requires 3 Discord | ||||
| # messages (each capped at a hardcoded 1950 bytes). | ||||
| # Default 1 | ||||
| MessageSplitMaxCount=3 | ||||
|  | ||||
| ################################################################### | ||||
| #telegram section | ||||
| ################################################################### | ||||
| @@ -988,6 +971,12 @@ DisableWebPagePreview=false | ||||
| #OPTIONAL (default false) | ||||
| UseFirstName=false | ||||
|  | ||||
| #If enabled use the "Full 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 and Last Name as Full Name | ||||
| #If all names are empty, username will be "unknown" | ||||
| #OPTIONAL (default false) | ||||
| UseFullName=false | ||||
|  | ||||
| #WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs | ||||
| #Those URLs will contain your bot-token. This may not be what you want. | ||||
| #For now there is no secure way to relay GIF/stickers/documents without seeing your token. | ||||
| @@ -1011,6 +1000,13 @@ QuoteFormat="{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" | ||||
| #OPTIONAL (default false) | ||||
| MediaConvertWebPToPNG=false | ||||
|  | ||||
| #Convert Tgs (Telegram animated sticker) images to PNG before upload. | ||||
| #This is useful when your bridge also contains platforms that do not support animated WebP files, like Discord. | ||||
| #This requires the external dependency `lottie`, which can be installed like this: | ||||
| #`pip install lottie cairosvg` | ||||
| #https://github.com/42wim/matterbridge/issues/874 | ||||
| #MediaConvertTgs="png" | ||||
|  | ||||
| #Disable sending of edits to other bridges | ||||
| #OPTIONAL (default false) | ||||
| EditDisable=false | ||||
| @@ -1044,7 +1040,7 @@ IgnoreMessages="^~~ badword" | ||||
| ReplaceMessages=[ ["cat","dog"] ] | ||||
|  | ||||
| #nicks you want to replace. | ||||
| #see replacemessages for syntaxa | ||||
| #see replacemessages for syntax | ||||
| #optional (default empty) | ||||
| ReplaceNicks=[ ["user--","user"] ] | ||||
|  | ||||
| @@ -1072,7 +1068,7 @@ Label="" | ||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| @@ -1086,6 +1082,12 @@ StripNick=false | ||||
| #OPTIONAL (default false) | ||||
| ShowTopicChange=false | ||||
|  | ||||
| #Opportunistically preserve threaded replies between Telegram groups. | ||||
| #This only works if the parent message is still in the cache. | ||||
| #Cache is flushed between restarts. | ||||
| #OPTIONAL (default false) | ||||
| PreserveThreading=false | ||||
|  | ||||
| ################################################################### | ||||
| #rocketchat section | ||||
| ################################################################### | ||||
| @@ -1179,7 +1181,7 @@ IgnoreMessages="^~~ badword" | ||||
| ReplaceMessages=[ ["cat","dog"] ] | ||||
|  | ||||
| #nicks you want to replace. | ||||
| #see replacemessages for syntaxa | ||||
| #see replacemessages for syntax | ||||
| #optional (default empty) | ||||
| ReplaceNicks=[ ["user--","user"] ] | ||||
|  | ||||
| @@ -1203,7 +1205,7 @@ Label="" | ||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| @@ -1230,12 +1232,16 @@ ShowTopicChange=false | ||||
| #REQUIRED | ||||
| Server="https://matrix.org" | ||||
|  | ||||
| #login/pass of your bot. | ||||
| #Authentication for your bot. | ||||
| #You can use either login/password OR mxid/token. The latter will be preferred if found. | ||||
| #Use a dedicated user for this and not your own! | ||||
| #Messages sent from this user will not be relayed to avoid loops. | ||||
| #REQUIRED | ||||
| Login="yourlogin" | ||||
| Password="yourpass" | ||||
| #OR | ||||
| MxID="@yourlogin:domain.tld" | ||||
| Token="tokenforthebotuser" | ||||
|  | ||||
| #Whether to send the homeserver suffix. eg ":matrix.org" in @username:matrix.org | ||||
| #to other bridges, or only send "username".(true only sends username) | ||||
| @@ -1253,12 +1259,14 @@ HTMLDisable=false | ||||
| # 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. | ||||
| #Useful if username overrides for incoming webhooks isn't enabled on the | ||||
| #matrix server. If you set PrefixMessagesWithNick to true, each message | ||||
| #from bridge to matrix will by default be prefixed by the RemoteNickFormat setting. i | ||||
| #OPTIONAL (default false) | ||||
| PrefixMessagesWithNick=false | ||||
| # Matrix quotes replies and as of matterbridge 1.24.0 we strip those as this causes | ||||
| # issues with bridges support threading and have PreserveThreading enabled. | ||||
| # But if you for example use mattermost or discord with webhooks you'll need to enable  | ||||
| # this (and keep PreserveThreading disabled) if you want something that looks like a reply from matrix. | ||||
| # See issues:  | ||||
| # - https://github.com/42wim/matterbridge/issues/1819 | ||||
| # - https://github.com/42wim/matterbridge/issues/1780 | ||||
| KeepQuotedReply=false | ||||
|  | ||||
| #Nicks you want to ignore. | ||||
| #Regular expressions supported | ||||
| @@ -1285,7 +1293,7 @@ IgnoreMessages="^~~ badword" | ||||
| ReplaceMessages=[ ["cat","dog"] ] | ||||
|  | ||||
| #nicks you want to replace. | ||||
| #see replacemessages for syntaxa | ||||
| #see replacemessages for syntax | ||||
| #optional (default empty) | ||||
| ReplaceNicks=[ ["user--","user"] ] | ||||
|  | ||||
| @@ -1309,10 +1317,15 @@ Label="" | ||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| #Rename the bot in the current room to the username of the message | ||||
| #This will make an additional API request per message and will probably count towards rate limits | ||||
| #OPTIONAL (default false) | ||||
| SpoofUsername=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) | ||||
| @@ -1377,7 +1390,7 @@ IgnoreMessages="^~~ badword" | ||||
| ReplaceMessages=[ ["cat","dog"] ] | ||||
|  | ||||
| #nicks you want to replace. | ||||
| #see replacemessages for syntaxa | ||||
| #see replacemessages for syntax | ||||
| #optional (default empty) | ||||
| ReplaceNicks=[ ["user--","user"] ] | ||||
|  | ||||
| @@ -1401,7 +1414,7 @@ Label="" | ||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| @@ -1416,9 +1429,7 @@ StripNick=false | ||||
| ShowTopicChange=false | ||||
|  | ||||
| ################################################################### | ||||
| # | ||||
| # NCTalk (Nextcloud Talk) | ||||
| # | ||||
| ################################################################### | ||||
|  | ||||
| [nctalk.bridge] | ||||
| @@ -1440,10 +1451,11 @@ Password = "talkuserpass" | ||||
| # Suffix for Guest Users | ||||
| GuestSuffix = " (Guest)" | ||||
|  | ||||
| # Separate display name (Note: needs to be configured from Nextcloud Talk to work) | ||||
| SeparateDisplayName=false | ||||
|  | ||||
| ################################################################### | ||||
| # | ||||
| # Mumble | ||||
| # | ||||
| ################################################################### | ||||
|  | ||||
| [mumble.bridge] | ||||
| @@ -1486,22 +1498,30 @@ TLSCACertificate=mumble-ca.crt | ||||
| # OPTIONAL (default false) | ||||
| SkipTLSVerify=false | ||||
|  | ||||
| #Message to show when a message is too big | ||||
| #Default "<clipped message>" | ||||
| MessageClipped="<clipped message>" | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| #Do not send joins/parts to other bridges | ||||
| #OPTIONAL (default false) | ||||
| NoSendJoinPart=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 | ||||
|  | ||||
| ################################################################### | ||||
| # | ||||
| # WhatsApp | ||||
| # | ||||
| ################################################################### | ||||
|  | ||||
| [whatsapp.bridge] | ||||
| @@ -1528,9 +1548,7 @@ Label="Organization" | ||||
|  | ||||
|  | ||||
| ################################################################### | ||||
| # | ||||
| # zulip | ||||
| # | ||||
| ################################################################### | ||||
|  | ||||
| [zulip] | ||||
| @@ -1581,7 +1599,7 @@ IgnoreMessages="^~~ badword" | ||||
| ReplaceMessages=[ ["cat","dog"] ] | ||||
|  | ||||
| #nicks you want to replace. | ||||
| #see replacemessages for syntaxa | ||||
| #see replacemessages for syntax | ||||
| #optional (default empty) | ||||
| ReplaceNicks=[ ["user--","user"] ] | ||||
|  | ||||
| @@ -1605,7 +1623,7 @@ Label="" | ||||
| RemoteNickFormat="[{PROTOCOL}] <{NICK}> " | ||||
|  | ||||
| #Enable to show users joins/parts from other bridges | ||||
| #Currently works for messages from the following bridges: irc, mattermost, slack, discord | ||||
| #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord | ||||
| #OPTIONAL (default false) | ||||
| ShowJoinPart=false | ||||
|  | ||||
| @@ -1619,6 +1637,18 @@ StripNick=false | ||||
| #OPTIONAL (default false) | ||||
| ShowTopicChange=false | ||||
|  | ||||
| ################################################################### | ||||
| # Harmony | ||||
| ################################################################### | ||||
|  | ||||
| [harmony.chat_harmonyapp_io] | ||||
| Homeserver = "https://chat.harmonyapp.io:2289" | ||||
| Token = "your token goes here" | ||||
| UserID = "user id of the bot account" | ||||
| Community = "community id that channels will be located in" | ||||
| UseUserName = true | ||||
| RemoteNickFormat = "{NICK}" | ||||
|  | ||||
| ################################################################### | ||||
| #API | ||||
| ################################################################### | ||||
| @@ -1664,6 +1694,7 @@ RemoteNickFormat="{NICK}" | ||||
|  | ||||
| #RemoteNickFormat defines how remote users appear on this bridge | ||||
| #The string "{NICK}" (case sensitive) will be replaced by the actual nick. | ||||
| #The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged. | ||||
| #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 "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge | ||||
| @@ -1690,7 +1721,7 @@ StripNick=false | ||||
| #The MediaServerDownload will be used so that bridges without native uploading support: | ||||
| #gitter, irc and xmpp will be shown links to the files on MediaServerDownload | ||||
| # | ||||
| #More information https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D | ||||
| #More information https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%28advanced%29 | ||||
| #OPTIONAL (default empty) | ||||
| MediaServerUpload="https://user:pass@yourserver.com/upload" | ||||
| #OPTIONAL (default empty) | ||||
| @@ -1819,7 +1850,7 @@ enable=true | ||||
|  | ||||
|     # account specified above | ||||
|     # REQUIRED | ||||
|     account="irc.freenode" | ||||
|     account="irc.libera" | ||||
|  | ||||
|     # The channel key in each gateway is mapped to a similar group chat ID on the chat platform | ||||
|     # To find the group chat ID for different platforms, refer to the table below | ||||
| @@ -1836,7 +1867,8 @@ enable=true | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|     #    irc     |      channel       |            #general           | The # symbol is required and should be lowercase! | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|     # mattermost |      channel       |            general            | This is the channel name as seen in the URL, not the display name | ||||
|     #            |      channel       |            general            | This is the channel name as seen in the URL, not the display name | ||||
|     # mattermost |    channel id      | ID:oc4wifyuojgw5f3nsuweesmz8w | This is the channel ID (only use if you know what you're doing) | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|     #   matrix   | #channel:server    |    #yourchannel:matrix.org    | Encrypted rooms are not supported in matrix | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
| @@ -1847,22 +1879,23 @@ enable=true | ||||
|     # rocketchat |      channel       |            #channel           | # is required for private channels too | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|     #   slack    |   channel name     |            general            | Do not include the # symbol | ||||
|     #            |    channel id      |           ID:C123456          | The underlying ID of a channel. This doesn't work with | ||||
|     #            |    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 | ||||
|     #            |   chatid/topicid   |          -123456789/12        | A large negative number/number. | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|     #  vk        |      peerid        |          2000000002           | A number that starts form 2000000000. Use --debug and send any message in chat to get PeerID in the logs  | ||||
|     #  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 | ||||
|     #   zulip    | stream/topic:topic |      general/topic:food       | Do not use the # when specifying a topic | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|  | ||||
|     # | ||||
| @@ -1877,7 +1910,7 @@ enable=true | ||||
|  | ||||
|     #[[gateway.out]] specifies the account and channels we will sent messages to. | ||||
|     [[gateway.out]] | ||||
|     account="irc.freenode" | ||||
|     account="irc.libera" | ||||
|     channel="#testing" | ||||
|  | ||||
|         #OPTIONAL - only used for IRC and XMPP protocols at the moment | ||||
| @@ -1911,6 +1944,10 @@ enable=true | ||||
|     account="zulip.streamchat" | ||||
|     channel="general/topic:mytopic" | ||||
|  | ||||
|     [[gateway.inout]] | ||||
|     account="harmony.chat_harmonyapp_io" | ||||
|     channel="channel id goes here" | ||||
|  | ||||
|     #API example | ||||
|     #[[gateway.inout]] | ||||
|     #account="api.local" | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| #WARNING: as this file contains credentials, be sure to set correct file permissions | ||||
| [irc] | ||||
|     [irc.freenode] | ||||
|     Server="irc.freenode.net:6667" | ||||
|     [irc.libera] | ||||
|     Server="irc.libera.chat:6667" | ||||
|     Nick="matterbot" | ||||
|  | ||||
| [mattermost] | ||||
| @@ -17,7 +17,7 @@ | ||||
| name="gateway1" | ||||
| enable=true | ||||
|     [[gateway.inout]] | ||||
|     account="irc.freenode" | ||||
|     account="irc.libera" | ||||
|     channel="#testing" | ||||
|  | ||||
|     [[gateway.inout]] | ||||
| @@ -29,6 +29,6 @@ enable=true | ||||
| #name="gateway2" | ||||
| #enable=true | ||||
| #inout = [ | ||||
| #    { account="irc.freenode", channel="#testing", options={key="channelkey"}}, | ||||
| #    { account="irc.libera", channel="#testing", options={key="channelkey"}}, | ||||
| #    { account="mattermost.work", channel="off-topic" }, | ||||
| #] | ||||
|   | ||||
							
								
								
									
										1
									
								
								matterclient/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| Find matterclient on https://github.com/matterbridge/matterclient | ||||
| @@ -1,226 +0,0 @@ | ||||
| package matterclient | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v5/model" | ||||
| ) | ||||
|  | ||||
| // GetChannels returns all channels we're members off | ||||
| func (m *MMClient) GetChannels() []*model.Channel { | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	var channels []*model.Channel | ||||
| 	// our primary team channels first | ||||
| 	channels = append(channels, m.Team.Channels...) | ||||
| 	for _, t := range m.OtherTeams { | ||||
| 		if t.Id != m.Team.Id { | ||||
| 			channels = append(channels, t.Channels...) | ||||
| 		} | ||||
| 	} | ||||
| 	return channels | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetChannelHeader(channelId string) string { //nolint:golint | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	for _, t := range m.OtherTeams { | ||||
| 		for _, channel := range append(t.Channels, t.MoreChannels...) { | ||||
| 			if channel.Id == channelId { | ||||
| 				return channel.Header | ||||
| 			} | ||||
|  | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func 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 | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	if teamId != "" { | ||||
| 		return m.getChannelIdTeam(name, teamId) | ||||
| 	} | ||||
|  | ||||
| 	for _, t := range m.OtherTeams { | ||||
| 		for _, channel := range append(t.Channels, t.MoreChannels...) { | ||||
| 			if getNormalisedName(channel) == name { | ||||
| 				return channel.Id | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *MMClient) getChannelIdTeam(name string, teamId string) string { //nolint:golint | ||||
| 	for _, t := range m.OtherTeams { | ||||
| 		if t.Id == teamId { | ||||
| 			for _, channel := range append(t.Channels, t.MoreChannels...) { | ||||
| 				if getNormalisedName(channel) == name { | ||||
| 					return channel.Id | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetChannelName(channelId string) string { //nolint:golint | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	for _, t := range m.OtherTeams { | ||||
| 		if t == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		for _, channel := range append(t.Channels, t.MoreChannels...) { | ||||
| 			if channel.Id == channelId { | ||||
| 				return getNormalisedName(channel) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetChannelTeamId(id string) string { //nolint:golint | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	for _, t := range append(m.OtherTeams, m.Team) { | ||||
| 		for _, channel := range append(t.Channels, t.MoreChannels...) { | ||||
| 			if channel.Id == id { | ||||
| 				return channel.TeamId | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetLastViewedAt(channelId string) int64 { //nolint:golint | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "") | ||||
| 	if resp.Error != nil { | ||||
| 		return model.GetMillis() | ||||
| 	} | ||||
| 	return res.LastViewedAt | ||||
| } | ||||
|  | ||||
| // GetMoreChannels returns existing channels where we're not a member off. | ||||
| func (m *MMClient) GetMoreChannels() []*model.Channel { | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	var channels []*model.Channel | ||||
| 	for _, t := range m.OtherTeams { | ||||
| 		channels = append(channels, t.MoreChannels...) | ||||
| 	} | ||||
| 	return channels | ||||
| } | ||||
|  | ||||
| // GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId). | ||||
| func (m *MMClient) GetTeamFromChannel(channelId string) string { //nolint:golint | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	var channels []*model.Channel | ||||
| 	for _, t := range m.OtherTeams { | ||||
| 		channels = append(channels, t.Channels...) | ||||
| 		if t.MoreChannels != nil { | ||||
| 			channels = append(channels, t.MoreChannels...) | ||||
| 		} | ||||
| 		for _, c := range channels { | ||||
| 			if c.Id == channelId { | ||||
| 				if c.Type == model.CHANNEL_GROUP { | ||||
| 					return "G" | ||||
| 				} | ||||
| 				return t.Id | ||||
| 			} | ||||
| 		} | ||||
| 		channels = nil | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	for _, c := range m.Team.Channels { | ||||
| 		if c.Id == channelId { | ||||
| 			m.logger.Debug("Not joining ", channelId, " already joined.") | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| 	m.logger.Debug("Joining ", channelId) | ||||
| 	_, resp := m.Client.AddChannelMember(channelId, m.User.Id) | ||||
| 	if resp.Error != nil { | ||||
| 		return resp.Error | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) UpdateChannelsTeam(teamID string) error { | ||||
| 	mmchannels, resp := m.Client.GetChannelsForTeamForUser(teamID, m.User.Id, 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 { | ||||
| 	if err := m.UpdateChannelsTeam(m.Team.Id); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, t := range m.OtherTeams { | ||||
| 		if err := m.UpdateChannelsTeam(t.Id); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) UpdateChannelHeader(channelId string, header string) { //nolint:golint | ||||
| 	channel := &model.Channel{Id: channelId, Header: header} | ||||
| 	m.logger.Debugf("updating channelheader %#v, %#v", channelId, header) | ||||
| 	_, resp := m.Client.UpdateChannel(channel) | ||||
| 	if resp.Error != nil { | ||||
| 		m.logger.Error(resp.Error) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *MMClient) UpdateLastViewed(channelId string) error { //nolint:golint | ||||
| 	m.logger.Debugf("posting lastview %#v", channelId) | ||||
| 	view := &model.ChannelView{ChannelId: channelId} | ||||
| 	_, resp := m.Client.ViewChannel(m.User.Id, view) | ||||
| 	if resp.Error != nil { | ||||
| 		m.logger.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error) | ||||
| 		return resp.Error | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @@ -1,297 +0,0 @@ | ||||
| package matterclient | ||||
|  | ||||
| import ( | ||||
| 	"crypto/md5" //nolint:gosec | ||||
| 	"crypto/tls" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/http/cookiejar" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"github.com/jpillora/backoff" | ||||
| 	"github.com/mattermost/mattermost-server/v5/model" | ||||
| ) | ||||
|  | ||||
| func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error { | ||||
| 	var resp *model.Response | ||||
| 	var appErr *model.AppError | ||||
| 	var logmsg = "trying login" | ||||
| 	var err error | ||||
| 	for { | ||||
| 		m.logger.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server) | ||||
| 		if m.Credentials.Token != "" { | ||||
| 			resp, err = m.doLoginToken() | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} else { | ||||
| 			m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass) | ||||
| 		} | ||||
| 		appErr = resp.Error | ||||
| 		if appErr != nil { | ||||
| 			d := b.Duration() | ||||
| 			m.logger.Debug(appErr.DetailedError) | ||||
| 			if firstConnection { | ||||
| 				if appErr.Message == "" { | ||||
| 					return errors.New(appErr.DetailedError) | ||||
| 				} | ||||
| 				return errors.New(appErr.Message) | ||||
| 			} | ||||
| 			m.logger.Debugf("LOGIN: %s, reconnecting in %s", appErr, d) | ||||
| 			time.Sleep(d) | ||||
| 			logmsg = "retrying login" | ||||
| 			continue | ||||
| 		} | ||||
| 		break | ||||
| 	} | ||||
| 	// reset timer | ||||
| 	b.Reset() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) doLoginToken() (*model.Response, error) { | ||||
| 	var resp *model.Response | ||||
| 	var logmsg = "trying login" | ||||
| 	m.Client.AuthType = model.HEADER_BEARER | ||||
| 	m.Client.AuthToken = m.Credentials.Token | ||||
| 	if m.Credentials.CookieToken { | ||||
| 		m.logger.Debugf(logmsg + " with cookie (MMAUTH) token") | ||||
| 		m.Client.HttpClient.Jar = m.createCookieJar(m.Credentials.Token) | ||||
| 	} else { | ||||
| 		m.logger.Debugf(logmsg + " with personal token") | ||||
| 	} | ||||
| 	m.User, resp = m.Client.GetMe("") | ||||
| 	if resp.Error != nil { | ||||
| 		return resp, resp.Error | ||||
| 	} | ||||
| 	if m.User == nil { | ||||
| 		m.logger.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass) | ||||
| 		return resp, errors.New("invalid token") | ||||
| 	} | ||||
| 	return resp, nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) handleLoginToken() error { | ||||
| 	switch { | ||||
| 	case strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN): | ||||
| 		token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=") | ||||
| 		if len(token) != 2 { | ||||
| 			return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken") | ||||
| 		} | ||||
| 		m.Credentials.Token = token[1] | ||||
| 		m.Credentials.CookieToken = true | ||||
| 	case strings.Contains(m.Credentials.Pass, "token="): | ||||
| 		token := strings.Split(m.Credentials.Pass, "token=") | ||||
| 		if len(token) != 2 { | ||||
| 			return errors.New("incorrect personal token. valid input is token=yourtoken") | ||||
| 		} | ||||
| 		m.Credentials.Token = token[1] | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) initClient(firstConnection bool, b *backoff.Backoff) error { | ||||
| 	uriScheme := "https://" | ||||
| 	if m.NoTLS { | ||||
| 		uriScheme = "http://" | ||||
| 	} | ||||
| 	// login to mattermost | ||||
| 	m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server) | ||||
| 	m.Client.HttpClient.Transport = &http.Transport{ | ||||
| 		TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec | ||||
| 		Proxy:           http.ProxyFromEnvironment, | ||||
| 	} | ||||
| 	m.Client.HttpClient.Timeout = time.Second * 10 | ||||
|  | ||||
| 	// handle MMAUTHTOKEN and personal token | ||||
| 	if err := m.handleLoginToken(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// check if server alive, retry until | ||||
| 	if err := m.serverAlive(firstConnection, b); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // initialize user and teams | ||||
| func (m *MMClient) initUser() error { | ||||
| 	m.Lock() | ||||
| 	defer m.Unlock() | ||||
| 	// we only load all team data on initial login. | ||||
| 	// all other updates are for channels from our (primary) team only. | ||||
| 	//m.logger.Debug("initUser(): loading all team data") | ||||
| 	teams, resp := m.Client.GetTeamsForUser(m.User.Id, "") | ||||
| 	if resp.Error != nil { | ||||
| 		return resp.Error | ||||
| 	} | ||||
| 	for _, team := range teams { | ||||
| 		idx := 0 | ||||
| 		max := 200 | ||||
| 		usermap := make(map[string]*model.User) | ||||
| 		mmusers, resp := m.Client.GetUsersInTeam(team.Id, idx, max, "") | ||||
| 		if resp.Error != nil { | ||||
| 			return errors.New(resp.Error.DetailedError) | ||||
| 		} | ||||
| 		for len(mmusers) > 0 { | ||||
| 			for _, user := range mmusers { | ||||
| 				usermap[user.Id] = user | ||||
| 			} | ||||
| 			mmusers, resp = m.Client.GetUsersInTeam(team.Id, idx, max, "") | ||||
| 			if resp.Error != nil { | ||||
| 				return errors.New(resp.Error.DetailedError) | ||||
| 			} | ||||
| 			idx++ | ||||
| 			time.Sleep(time.Millisecond * 200) | ||||
| 		} | ||||
| 		m.logger.Infof("found %d users in team %s", len(usermap), team.Name) | ||||
|  | ||||
| 		t := &Team{Team: team, Users: usermap, Id: team.Id} | ||||
|  | ||||
| 		mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, false, "") | ||||
| 		if resp.Error != nil { | ||||
| 			return resp.Error | ||||
| 		} | ||||
| 		t.Channels = mmchannels | ||||
| 		mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "") | ||||
| 		if resp.Error != nil { | ||||
| 			return resp.Error | ||||
| 		} | ||||
| 		t.MoreChannels = mmchannels | ||||
| 		m.OtherTeams = append(m.OtherTeams, t) | ||||
| 		if team.Name == m.Credentials.Team { | ||||
| 			m.Team = t | ||||
| 			m.logger.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id) | ||||
| 		} | ||||
| 		// add all users | ||||
| 		for k, v := range t.Users { | ||||
| 			m.Users[k] = v | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error { | ||||
| 	defer b.Reset() | ||||
| 	for { | ||||
| 		d := b.Duration() | ||||
| 		// bogus call to get the serverversion | ||||
| 		_, resp := m.Client.Logout() | ||||
| 		if resp.Error != nil { | ||||
| 			return fmt.Errorf("%#v", resp.Error.Error()) | ||||
| 		} | ||||
| 		if firstConnection && !m.SkipVersionCheck && !supportedVersion(resp.ServerVersion) { | ||||
| 			return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion) | ||||
| 		} | ||||
| 		if !m.SkipVersionCheck { | ||||
| 			m.ServerVersion = resp.ServerVersion | ||||
| 			if m.ServerVersion == "" { | ||||
| 				m.logger.Debugf("Server not up yet, reconnecting in %s", d) | ||||
| 				time.Sleep(d) | ||||
| 			} else { | ||||
| 				m.logger.Infof("Found version %s", m.ServerVersion) | ||||
| 				return nil | ||||
| 			} | ||||
| 		} else { | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *MMClient) wsConnect() { | ||||
| 	b := &backoff.Backoff{ | ||||
| 		Min:    time.Second, | ||||
| 		Max:    5 * time.Minute, | ||||
| 		Jitter: true, | ||||
| 	} | ||||
|  | ||||
| 	m.WsConnected = false | ||||
| 	wsScheme := "wss://" | ||||
| 	if m.NoTLS { | ||||
| 		wsScheme = "ws://" | ||||
| 	} | ||||
|  | ||||
| 	// setup websocket connection | ||||
| 	wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket" | ||||
| 	header := http.Header{} | ||||
| 	header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken) | ||||
|  | ||||
| 	m.logger.Debugf("WsClient: making connection: %s", wsurl) | ||||
| 	for { | ||||
| 		wsDialer := &websocket.Dialer{ | ||||
| 			TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec | ||||
| 			Proxy:           http.ProxyFromEnvironment, | ||||
| 		} | ||||
| 		var err error | ||||
| 		m.WsClient, _, err = wsDialer.Dial(wsurl, header) | ||||
| 		if err != nil { | ||||
| 			d := b.Duration() | ||||
| 			m.logger.Debugf("WSS: %s, reconnecting in %s", err, d) | ||||
| 			time.Sleep(d) | ||||
| 			continue | ||||
| 		} | ||||
| 		break | ||||
| 	} | ||||
|  | ||||
| 	m.logger.Debug("WsClient: connected") | ||||
| 	m.WsSequence = 1 | ||||
| 	m.WsPingChan = make(chan *model.WebSocketResponse) | ||||
| 	// only start to parse WS messages when login is completely done | ||||
| 	m.WsConnected = true | ||||
| } | ||||
|  | ||||
| func (m *MMClient) createCookieJar(token string) *cookiejar.Jar { | ||||
| 	var cookies []*http.Cookie | ||||
| 	jar, _ := cookiejar.New(nil) | ||||
| 	firstCookie := &http.Cookie{ | ||||
| 		Name:   "MMAUTHTOKEN", | ||||
| 		Value:  token, | ||||
| 		Path:   "/", | ||||
| 		Domain: m.Credentials.Server, | ||||
| 	} | ||||
| 	cookies = append(cookies, firstCookie) | ||||
| 	cookieURL, _ := url.Parse("https://" + m.Credentials.Server) | ||||
| 	jar.SetCookies(cookieURL, cookies) | ||||
| 	return jar | ||||
| } | ||||
|  | ||||
| func (m *MMClient) checkAlive() error { | ||||
| 	// check if session still is valid | ||||
| 	_, resp := m.Client.GetMe("") | ||||
| 	if resp.Error != nil { | ||||
| 		return resp.Error | ||||
| 	} | ||||
| 	m.logger.Debug("WS PING") | ||||
| 	return m.sendWSRequest("ping", nil) | ||||
| } | ||||
|  | ||||
| func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error { | ||||
| 	req := &model.WebSocketRequest{} | ||||
| 	req.Seq = m.WsSequence | ||||
| 	req.Action = action | ||||
| 	req.Data = data | ||||
| 	m.WsSequence++ | ||||
| 	m.logger.Debugf("sendWsRequest %#v", req) | ||||
| 	return m.WsClient.WriteJSON(req) | ||||
| } | ||||
|  | ||||
| func supportedVersion(version string) bool { | ||||
| 	if strings.HasPrefix(version, "3.8.0") || | ||||
| 		strings.HasPrefix(version, "3.9.0") || | ||||
| 		strings.HasPrefix(version, "3.10.0") || | ||||
| 		strings.HasPrefix(version, "4.") || | ||||
| 		strings.HasPrefix(version, "5.") { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func digestString(s string) string { | ||||
| 	return fmt.Sprintf("%x", md5.Sum([]byte(s))) //nolint:gosec | ||||
| } | ||||
| @@ -1,294 +0,0 @@ | ||||
| package matterclient | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	lru "github.com/hashicorp/golang-lru" | ||||
| 	"github.com/jpillora/backoff" | ||||
| 	prefixed "github.com/matterbridge/logrus-prefixed-formatter" | ||||
| 	"github.com/mattermost/mattermost-server/v5/model" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| type Credentials struct { | ||||
| 	Login            string | ||||
| 	Team             string | ||||
| 	Pass             string | ||||
| 	Token            string | ||||
| 	CookieToken      bool | ||||
| 	Server           string | ||||
| 	NoTLS            bool | ||||
| 	SkipTLSVerify    bool | ||||
| 	SkipVersionCheck bool | ||||
| } | ||||
|  | ||||
| type Message struct { | ||||
| 	Raw      *model.WebSocketEvent | ||||
| 	Post     *model.Post | ||||
| 	Team     string | ||||
| 	Channel  string | ||||
| 	Username string | ||||
| 	Text     string | ||||
| 	Type     string | ||||
| 	UserID   string | ||||
| } | ||||
|  | ||||
| //nolint:golint | ||||
| type Team struct { | ||||
| 	Team         *model.Team | ||||
| 	Id           string | ||||
| 	Channels     []*model.Channel | ||||
| 	MoreChannels []*model.Channel | ||||
| 	Users        map[string]*model.User | ||||
| } | ||||
|  | ||||
| type MMClient struct { | ||||
| 	sync.RWMutex | ||||
| 	*Credentials | ||||
|  | ||||
| 	Team          *Team | ||||
| 	OtherTeams    []*Team | ||||
| 	Client        *model.Client4 | ||||
| 	User          *model.User | ||||
| 	Users         map[string]*model.User | ||||
| 	MessageChan   chan *Message | ||||
| 	WsClient      *websocket.Conn | ||||
| 	WsQuit        bool | ||||
| 	WsAway        bool | ||||
| 	WsConnected   bool | ||||
| 	WsSequence    int64 | ||||
| 	WsPingChan    chan *model.WebSocketResponse | ||||
| 	ServerVersion string | ||||
| 	OnWsConnect   func() | ||||
|  | ||||
| 	logger     *logrus.Entry | ||||
| 	rootLogger *logrus.Logger | ||||
| 	lruCache   *lru.Cache | ||||
| 	allevents  bool | ||||
| } | ||||
|  | ||||
| // New will instantiate a new Matterclient with the specified login details without connecting. | ||||
| func New(login string, pass string, team string, server string) *MMClient { | ||||
| 	rootLogger := logrus.New() | ||||
| 	rootLogger.SetFormatter(&prefixed.TextFormatter{ | ||||
| 		PrefixPadding: 13, | ||||
| 		DisableColors: true, | ||||
| 	}) | ||||
|  | ||||
| 	cred := &Credentials{ | ||||
| 		Login:  login, | ||||
| 		Pass:   pass, | ||||
| 		Team:   team, | ||||
| 		Server: server, | ||||
| 	} | ||||
|  | ||||
| 	cache, _ := lru.New(500) | ||||
| 	return &MMClient{ | ||||
| 		Credentials: cred, | ||||
| 		MessageChan: make(chan *Message, 100), | ||||
| 		Users:       make(map[string]*model.User), | ||||
| 		rootLogger:  rootLogger, | ||||
| 		lruCache:    cache, | ||||
| 		logger:      rootLogger.WithFields(logrus.Fields{"prefix": "matterclient"}), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SetDebugLog activates debugging logging on all Matterclient log output. | ||||
| func (m *MMClient) SetDebugLog() { | ||||
| 	m.rootLogger.SetFormatter(&prefixed.TextFormatter{ | ||||
| 		PrefixPadding:   13, | ||||
| 		DisableColors:   true, | ||||
| 		FullTimestamp:   false, | ||||
| 		ForceFormatting: true, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // SetLogLevel tries to parse the specified level and if successful sets | ||||
| // the log level accordingly. Accepted levels are: 'debug', 'info', 'warn', | ||||
| // 'error', 'fatal' and 'panic'. | ||||
| func (m *MMClient) SetLogLevel(level string) { | ||||
| 	l, err := logrus.ParseLevel(level) | ||||
| 	if err != nil { | ||||
| 		m.logger.Warnf("Failed to parse specified log-level '%s': %#v", level, err) | ||||
| 	} else { | ||||
| 		m.rootLogger.SetLevel(l) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *MMClient) EnableAllEvents() { | ||||
| 	m.allevents = true | ||||
| } | ||||
|  | ||||
| // Login tries to connect the client with the loging details with which it was initialized. | ||||
| func (m *MMClient) Login() error { | ||||
| 	// check if this is a first connect or a reconnection | ||||
| 	firstConnection := true | ||||
| 	if m.WsConnected { | ||||
| 		firstConnection = false | ||||
| 	} | ||||
| 	m.WsConnected = false | ||||
| 	if m.WsQuit { | ||||
| 		return nil | ||||
| 	} | ||||
| 	b := &backoff.Backoff{ | ||||
| 		Min:    time.Second, | ||||
| 		Max:    5 * time.Minute, | ||||
| 		Jitter: true, | ||||
| 	} | ||||
|  | ||||
| 	// do initialization setup | ||||
| 	if err := m.initClient(firstConnection, b); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err := m.doLogin(firstConnection, b); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err := m.initUser(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if m.Team == nil { | ||||
| 		validTeamNames := make([]string, len(m.OtherTeams)) | ||||
| 		for i, t := range m.OtherTeams { | ||||
| 			validTeamNames[i] = t.Team.Name | ||||
| 		} | ||||
| 		return fmt.Errorf("Team '%s' not found in %v", m.Credentials.Team, validTeamNames) | ||||
| 	} | ||||
|  | ||||
| 	m.wsConnect() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Logout disconnects the client from the chat server. | ||||
| func (m *MMClient) Logout() error { | ||||
| 	m.logger.Debugf("logout as %s (team: %s) on %s", m.Credentials.Login, m.Credentials.Team, m.Credentials.Server) | ||||
| 	m.WsQuit = true | ||||
| 	m.WsClient.Close() | ||||
| 	m.WsClient.UnderlyingConn().Close() | ||||
| 	if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) { | ||||
| 		m.logger.Debug("Not invalidating session in logout, credential is a token") | ||||
| 		return nil | ||||
| 	} | ||||
| 	_, resp := m.Client.Logout() | ||||
| 	if resp.Error != nil { | ||||
| 		return resp.Error | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // WsReceiver implements the core loop that manages the connection to the chat server. In | ||||
| // case of a disconnect it will try to reconnect. A call to this method is blocking until | ||||
| // the 'WsQuite' field of the MMClient object is set to 'true'. | ||||
| func (m *MMClient) WsReceiver() { | ||||
| 	for { | ||||
| 		var rawMsg json.RawMessage | ||||
| 		var err error | ||||
|  | ||||
| 		if m.WsQuit { | ||||
| 			m.logger.Debug("exiting WsReceiver") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if !m.WsConnected { | ||||
| 			time.Sleep(time.Millisecond * 100) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil { | ||||
| 			m.logger.Error("error:", err) | ||||
| 			// reconnect | ||||
| 			m.wsConnect() | ||||
| 		} | ||||
|  | ||||
| 		var event model.WebSocketEvent | ||||
| 		if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() { | ||||
| 			m.logger.Debugf("WsReceiver event: %#v", event) | ||||
| 			msg := &Message{Raw: &event, Team: m.Credentials.Team} | ||||
| 			m.parseMessage(msg) | ||||
| 			// check if we didn't empty the message | ||||
| 			if msg.Text != "" { | ||||
| 				m.MessageChan <- msg | ||||
| 				continue | ||||
| 			} | ||||
| 			// if we have file attached but the message is empty, also send it | ||||
| 			if msg.Post != nil { | ||||
| 				if msg.Text != "" || len(msg.Post.FileIds) > 0 || msg.Post.Type == "slack_attachment" { | ||||
| 					m.MessageChan <- msg | ||||
| 					continue | ||||
| 				} | ||||
| 			} | ||||
| 			if m.allevents { | ||||
| 				m.MessageChan <- msg | ||||
| 				continue | ||||
| 			} | ||||
| 			switch msg.Raw.Event { | ||||
| 			case model.WEBSOCKET_EVENT_USER_ADDED, | ||||
| 				model.WEBSOCKET_EVENT_USER_REMOVED, | ||||
| 				model.WEBSOCKET_EVENT_CHANNEL_CREATED, | ||||
| 				model.WEBSOCKET_EVENT_CHANNEL_DELETED: | ||||
| 				m.MessageChan <- msg | ||||
| 				continue | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		var response model.WebSocketResponse | ||||
| 		if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() { | ||||
| 			m.logger.Debugf("WsReceiver response: %#v", response) | ||||
| 			m.parseResponse(response) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // StatusLoop implements a ping-cycle that ensures that the connection to the chat servers | ||||
| // remains alive. In case of a disconnect it will try to reconnect. A call to this method | ||||
| // is blocking until the 'WsQuite' field of the MMClient object is set to 'true'. | ||||
| func (m *MMClient) StatusLoop() { | ||||
| 	retries := 0 | ||||
| 	backoff := time.Second * 60 | ||||
| 	if m.OnWsConnect != nil { | ||||
| 		m.OnWsConnect() | ||||
| 	} | ||||
| 	m.logger.Debug("StatusLoop:", m.OnWsConnect != nil) | ||||
| 	for { | ||||
| 		if m.WsQuit { | ||||
| 			return | ||||
| 		} | ||||
| 		if m.WsConnected { | ||||
| 			if err := m.checkAlive(); err != nil { | ||||
| 				m.logger.Errorf("Connection is not alive: %#v", err) | ||||
| 			} | ||||
| 			select { | ||||
| 			case <-m.WsPingChan: | ||||
| 				m.logger.Debug("WS PONG received") | ||||
| 				backoff = time.Second * 60 | ||||
| 			case <-time.After(time.Second * 5): | ||||
| 				if retries > 3 { | ||||
| 					m.logger.Debug("StatusLoop() timeout") | ||||
| 					m.Logout() | ||||
| 					m.WsQuit = false | ||||
| 					err := m.Login() | ||||
| 					if err != nil { | ||||
| 						m.logger.Errorf("Login failed: %#v", err) | ||||
| 						break | ||||
| 					} | ||||
| 					if m.OnWsConnect != nil { | ||||
| 						m.OnWsConnect() | ||||
| 					} | ||||
| 					go m.WsReceiver() | ||||
| 				} else { | ||||
| 					retries++ | ||||
| 					backoff = time.Second * 5 | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		time.Sleep(backoff) | ||||
| 	} | ||||
| } | ||||
| @@ -1,207 +0,0 @@ | ||||
| package matterclient | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v5/model" | ||||
| ) | ||||
|  | ||||
| func (m *MMClient) parseActionPost(rmsg *Message) { | ||||
| 	// add post to cache, if it already exists don't relay this again. | ||||
| 	// this should fix reposts | ||||
| 	if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok { | ||||
| 		m.logger.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string)) | ||||
| 		rmsg.Text = "" | ||||
| 		return | ||||
| 	} | ||||
| 	data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string))) | ||||
| 	// we don't have the user, refresh the userlist | ||||
| 	if m.GetUser(data.UserId) == nil { | ||||
| 		m.logger.Infof("User '%v' is not known, ignoring message '%#v'", | ||||
| 			data.UserId, data) | ||||
| 		return | ||||
| 	} | ||||
| 	rmsg.Username = m.GetUserName(data.UserId) | ||||
| 	rmsg.Channel = m.GetChannelName(data.ChannelId) | ||||
| 	rmsg.UserID = data.UserId | ||||
| 	rmsg.Type = data.Type | ||||
| 	teamid, _ := rmsg.Raw.Data["team_id"].(string) | ||||
| 	// edit messsages have no team_id for some reason | ||||
| 	if teamid == "" { | ||||
| 		// we can find the team_id from the channelid | ||||
| 		teamid = m.GetChannelTeamId(data.ChannelId) | ||||
| 		rmsg.Raw.Data["team_id"] = teamid | ||||
| 	} | ||||
| 	if teamid != "" { | ||||
| 		rmsg.Team = m.GetTeamName(teamid) | ||||
| 	} | ||||
| 	// direct message | ||||
| 	if rmsg.Raw.Data["channel_type"] == "D" { | ||||
| 		rmsg.Channel = m.GetUser(data.UserId).Username | ||||
| 	} | ||||
| 	rmsg.Text = data.Message | ||||
| 	rmsg.Post = data | ||||
| } | ||||
|  | ||||
| func (m *MMClient) parseMessage(rmsg *Message) { | ||||
| 	switch rmsg.Raw.Event { | ||||
| 	case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED: | ||||
| 		m.parseActionPost(rmsg) | ||||
| 	case "user_updated": | ||||
| 		user := rmsg.Raw.Data["user"].(map[string]interface{}) | ||||
| 		if _, ok := user["id"].(string); ok { | ||||
| 			m.UpdateUser(user["id"].(string)) | ||||
| 		} | ||||
| 	case "group_added": | ||||
| 		if err := m.UpdateChannels(); err != nil { | ||||
| 			m.logger.Errorf("failed to update channels: %#v", err) | ||||
| 		} | ||||
| 		/* | ||||
| 			case model.ACTION_USER_REMOVED: | ||||
| 				m.handleWsActionUserRemoved(&rmsg) | ||||
| 			case model.ACTION_USER_ADDED: | ||||
| 				m.handleWsActionUserAdded(&rmsg) | ||||
| 		*/ | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) { | ||||
| 	if rmsg.Data != nil { | ||||
| 		// ping reply | ||||
| 		if rmsg.Data["text"].(string) == "pong" { | ||||
| 			m.WsPingChan <- &rmsg | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *MMClient) DeleteMessage(postId string) error { //nolint:golint | ||||
| 	_, resp := m.Client.DeletePost(postId) | ||||
| 	if resp.Error != nil { | ||||
| 		return resp.Error | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint | ||||
| 	post := &model.Post{Message: text, Id: postId} | ||||
| 	res, resp := m.Client.UpdatePost(postId, post) | ||||
| 	if resp.Error != nil { | ||||
| 		return "", resp.Error | ||||
| 	} | ||||
| 	return res.Id, nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetFileLinks(filenames []string) []string { | ||||
| 	uriScheme := "https://" | ||||
| 	if m.NoTLS { | ||||
| 		uriScheme = "http://" | ||||
| 	} | ||||
|  | ||||
| 	var output []string | ||||
| 	for _, f := range filenames { | ||||
| 		res, resp := m.Client.GetFileLink(f) | ||||
| 		if resp.Error != nil { | ||||
| 			// public links is probably disabled, create the link ourselves | ||||
| 			output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V4+"/files/"+f) | ||||
| 			continue | ||||
| 		} | ||||
| 		output = append(output, res) | ||||
| 	} | ||||
| 	return output | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList { //nolint:golint | ||||
| 	res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "") | ||||
| 	if resp.Error != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList { //nolint:golint | ||||
| 	res, resp := m.Client.GetPostsSince(channelId, time) | ||||
| 	if resp.Error != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetPublicLink(filename string) string { | ||||
| 	res, resp := m.Client.GetFileLink(filename) | ||||
| 	if resp.Error != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetPublicLinks(filenames []string) []string { | ||||
| 	var output []string | ||||
| 	for _, f := range filenames { | ||||
| 		res, resp := m.Client.GetFileLink(f) | ||||
| 		if resp.Error != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		output = append(output, res) | ||||
| 	} | ||||
| 	return output | ||||
| } | ||||
|  | ||||
| func (m *MMClient) PostMessage(channelId string, text string, rootId string) (string, error) { //nolint:golint | ||||
| 	post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId} | ||||
| 	res, resp := m.Client.CreatePost(post) | ||||
| 	if resp.Error != nil { | ||||
| 		return "", resp.Error | ||||
| 	} | ||||
| 	return res.Id, nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) PostMessageWithFiles(channelId string, text string, rootId string, fileIds []string) (string, error) { //nolint:golint | ||||
| 	post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId, FileIds: fileIds} | ||||
| 	res, resp := m.Client.CreatePost(post) | ||||
| 	if resp.Error != nil { | ||||
| 		return "", resp.Error | ||||
| 	} | ||||
| 	return res.Id, nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) SearchPosts(query string) *model.PostList { | ||||
| 	res, resp := m.Client.SearchPosts(m.Team.Id, query, false) | ||||
| 	if resp.Error != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| // SendDirectMessage sends a direct message to specified user | ||||
| func (m *MMClient) SendDirectMessage(toUserId string, msg string, rootId string) { //nolint:golint | ||||
| 	m.SendDirectMessageProps(toUserId, msg, rootId, nil) | ||||
| } | ||||
|  | ||||
| func (m *MMClient) SendDirectMessageProps(toUserId string, msg string, rootId string, props map[string]interface{}) { //nolint:golint | ||||
| 	m.logger.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg) | ||||
| 	// create DM channel (only happens on first message) | ||||
| 	_, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId) | ||||
| 	if resp.Error != nil { | ||||
| 		m.logger.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error) | ||||
| 		return | ||||
| 	} | ||||
| 	channelName := model.GetDMNameFromIds(toUserId, m.User.Id) | ||||
|  | ||||
| 	// update our channels | ||||
| 	if err := m.UpdateChannels(); err != nil { | ||||
| 		m.logger.Errorf("failed to update channels: %#v", err) | ||||
| 	} | ||||
|  | ||||
| 	// build & send the message | ||||
| 	msg = strings.Replace(msg, "\r", "", -1) | ||||
| 	post := &model.Post{ChannelId: m.GetChannelId(channelName, m.Team.Id), Message: msg, RootId: rootId, Props: props} | ||||
| 	m.Client.CreatePost(post) | ||||
| } | ||||
|  | ||||
| func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) { //nolint:golint | ||||
| 	f, resp := m.Client.UploadFile(data, channelId, filename) | ||||
| 	if resp.Error != nil { | ||||
| 		return "", resp.Error | ||||
| 	} | ||||
| 	return f.FileInfos[0].Id, nil | ||||
| } | ||||
| @@ -1,165 +0,0 @@ | ||||
| package matterclient | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v5/model" | ||||
| ) | ||||
|  | ||||
| func (m *MMClient) GetNickName(userId string) string { //nolint:golint | ||||
| 	user := m.GetUser(userId) | ||||
| 	if user != nil { | ||||
| 		return user.Nickname | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetStatus(userId string) string { //nolint:golint | ||||
| 	res, resp := m.Client.GetUserStatus(userId, "") | ||||
| 	if resp.Error != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	if res.Status == model.STATUS_AWAY { | ||||
| 		return "away" | ||||
| 	} | ||||
| 	if res.Status == model.STATUS_ONLINE { | ||||
| 		return "online" | ||||
| 	} | ||||
| 	return "offline" | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetStatuses() map[string]string { | ||||
| 	var ids []string | ||||
| 	statuses := make(map[string]string) | ||||
| 	for id := range m.Users { | ||||
| 		ids = append(ids, id) | ||||
| 	} | ||||
| 	res, resp := m.Client.GetUsersStatusesByIds(ids) | ||||
| 	if resp.Error != nil { | ||||
| 		return statuses | ||||
| 	} | ||||
| 	for _, status := range res { | ||||
| 		statuses[status.UserId] = "offline" | ||||
| 		if status.Status == model.STATUS_AWAY { | ||||
| 			statuses[status.UserId] = "away" | ||||
| 		} | ||||
| 		if status.Status == model.STATUS_ONLINE { | ||||
| 			statuses[status.UserId] = "online" | ||||
| 		} | ||||
| 	} | ||||
| 	return statuses | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetTeamId() string { //nolint:golint | ||||
| 	return m.Team.Id | ||||
| } | ||||
|  | ||||
| // GetTeamName returns the name of the specified teamId | ||||
| func (m *MMClient) GetTeamName(teamId string) string { //nolint:golint | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	for _, t := range m.OtherTeams { | ||||
| 		if t.Id == teamId { | ||||
| 			return t.Team.Name | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetUser(userId string) *model.User { //nolint:golint | ||||
| 	m.Lock() | ||||
| 	defer m.Unlock() | ||||
| 	_, ok := m.Users[userId] | ||||
| 	if !ok { | ||||
| 		res, resp := m.Client.GetUser(userId, "") | ||||
| 		if resp.Error != nil { | ||||
| 			return nil | ||||
| 		} | ||||
| 		m.Users[userId] = res | ||||
| 	} | ||||
| 	return m.Users[userId] | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetUserName(userId string) string { //nolint:golint | ||||
| 	user := m.GetUser(userId) | ||||
| 	if user != nil { | ||||
| 		return user.Username | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *MMClient) GetUsers() map[string]*model.User { | ||||
| 	users := make(map[string]*model.User) | ||||
| 	m.RLock() | ||||
| 	defer m.RUnlock() | ||||
| 	for k, v := range m.Users { | ||||
| 		users[k] = v | ||||
| 	} | ||||
| 	return users | ||||
| } | ||||
|  | ||||
| func (m *MMClient) UpdateUsers() error { | ||||
| 	idx := 0 | ||||
| 	max := 200 | ||||
| 	mmusers, resp := m.Client.GetUsers(idx, max, "") | ||||
| 	if resp.Error != nil { | ||||
| 		return errors.New(resp.Error.DetailedError) | ||||
| 	} | ||||
| 	for len(mmusers) > 0 { | ||||
| 		m.Lock() | ||||
| 		for _, user := range mmusers { | ||||
| 			m.Users[user.Id] = user | ||||
| 		} | ||||
| 		m.Unlock() | ||||
| 		mmusers, resp = m.Client.GetUsers(idx, max, "") | ||||
| 		time.Sleep(time.Millisecond * 300) | ||||
| 		if resp.Error != nil { | ||||
| 			return errors.New(resp.Error.DetailedError) | ||||
| 		} | ||||
| 		idx++ | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) UpdateUserNick(nick string) error { | ||||
| 	user := m.User | ||||
| 	user.Nickname = nick | ||||
| 	_, resp := m.Client.UpdateUser(user) | ||||
| 	if resp.Error != nil { | ||||
| 		return resp.Error | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) UsernamesInChannel(channelId string) []string { //nolint:golint | ||||
| 	res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "") | ||||
| 	if resp.Error != nil { | ||||
| 		m.logger.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error) | ||||
| 		return []string{} | ||||
| 	} | ||||
| 	allusers := m.GetUsers() | ||||
| 	result := []string{} | ||||
| 	for _, member := range *res { | ||||
| 		result = append(result, allusers[member.UserId].Nickname) | ||||
| 	} | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func (m *MMClient) UpdateStatus(userId string, status string) error { //nolint:golint | ||||
| 	_, resp := m.Client.UpdateUserStatus(userId, &model.Status{Status: status}) | ||||
| 	if resp.Error != nil { | ||||
| 		return resp.Error | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *MMClient) UpdateUser(userId string) { //nolint:golint | ||||
| 	m.Lock() | ||||
| 	defer m.Unlock() | ||||
| 	res, resp := m.Client.GetUser(userId, "") | ||||
| 	if resp.Error != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	m.Users[userId] = res | ||||
| } | ||||
| @@ -1,21 +1,18 @@ | ||||
| FROM alpine AS builder | ||||
|  | ||||
| COPY . /go/src/github.com/42wim/matterbridge | ||||
| COPY . /go/src/matterbridge | ||||
| RUN apk add \ | ||||
|     go \ | ||||
|     git \ | ||||
|     gcc \ | ||||
|     musl-dev \ | ||||
|   && cd /go/src/github.com/42wim/matterbridge \ | ||||
|   && export GOPATH=/go \ | ||||
|   && go get \ | ||||
|   && go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge | ||||
|   && cd /go/src/matterbridge \ | ||||
|   && CGO_ENABLED=0 go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge | ||||
|  | ||||
| FROM alpine | ||||
| RUN apk --no-cache add \ | ||||
|     ca-certificates \ | ||||
|     cairo \ | ||||
|     libjpeg-turbo \ | ||||
|     libwebp-dev \ | ||||
|     mailcap \ | ||||
|     py3-webencodings \ | ||||
|     python3 \ | ||||
|   | ||||
							
								
								
									
										27
									
								
								vendor/filippo.io/edwards25519/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| Copyright (c) 2009 The Go Authors. All rights reserved. | ||||
|  | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are | ||||
| met: | ||||
|  | ||||
|    * Redistributions of source code must retain the above copyright | ||||
| notice, this list of conditions and the following disclaimer. | ||||
|    * Redistributions in binary form must reproduce the above | ||||
| copyright notice, this list of conditions and the following disclaimer | ||||
| in the documentation and/or other materials provided with the | ||||
| distribution. | ||||
|    * Neither the name of Google Inc. nor the names of its | ||||
| contributors may be used to endorse or promote products derived from | ||||
| this software without specific prior written permission. | ||||
|  | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
							
								
								
									
										14
									
								
								vendor/filippo.io/edwards25519/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| # filippo.io/edwards25519 | ||||
|  | ||||
| ``` | ||||
| import "filippo.io/edwards25519" | ||||
| ``` | ||||
|  | ||||
| This library implements the edwards25519 elliptic curve, exposing the necessary APIs to build a wide array of higher-level primitives. | ||||
| Read the docs at [pkg.go.dev/filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519). | ||||
|  | ||||
| The code is originally derived from Adam Langley's internal implementation in the Go standard library, and includes George Tankersley's [performance improvements](https://golang.org/cl/71950). It was then further developed by Henry de Valence for use in ristretto255, and was finally [merged back into the Go standard library](https://golang.org/cl/276272) as of Go 1.17. It now tracks the upstream codebase and extends it with additional functionality. | ||||
|  | ||||
| Most users don't need this package, and should instead use `crypto/ed25519` for signatures, `golang.org/x/crypto/curve25519` for Diffie-Hellman, or `github.com/gtank/ristretto255` for prime order group logic. However, for anyone currently using a fork of `crypto/internal/edwards25519`/`crypto/ed25519/internal/edwards25519` or `github.com/agl/edwards25519`, this package should be a safer, faster, and more powerful alternative. | ||||
|  | ||||
| Since this package is meant to curb proliferation of edwards25519 implementations in the Go ecosystem, it welcomes requests for new APIs or reviewable performance improvements. | ||||
							
								
								
									
										20
									
								
								vendor/filippo.io/edwards25519/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | ||||
| // Copyright (c) 2021 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| // Package edwards25519 implements group logic for the twisted Edwards curve | ||||
| // | ||||
| //	-x^2 + y^2 = 1 + -(121665/121666)*x^2*y^2 | ||||
| // | ||||
| // This is better known as the Edwards curve equivalent to Curve25519, and is | ||||
| // the curve used by the Ed25519 signature scheme. | ||||
| // | ||||
| // Most users don't need this package, and should instead use crypto/ed25519 for | ||||
| // signatures, golang.org/x/crypto/curve25519 for Diffie-Hellman, or | ||||
| // github.com/gtank/ristretto255 for prime order group logic. | ||||
| // | ||||
| // However, developers who do need to interact with low-level edwards25519 | ||||
| // operations can use this package, which is an extended version of | ||||
| // crypto/internal/edwards25519 from the standard library repackaged as | ||||
| // an importable module. | ||||
| package edwards25519 | ||||
							
								
								
									
										427
									
								
								vendor/filippo.io/edwards25519/edwards25519.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,427 @@ | ||||
| // Copyright (c) 2017 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package edwards25519 | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
|  | ||||
| 	"filippo.io/edwards25519/field" | ||||
| ) | ||||
|  | ||||
| // Point types. | ||||
|  | ||||
| type projP1xP1 struct { | ||||
| 	X, Y, Z, T field.Element | ||||
| } | ||||
|  | ||||
| type projP2 struct { | ||||
| 	X, Y, Z field.Element | ||||
| } | ||||
|  | ||||
| // Point represents a point on the edwards25519 curve. | ||||
| // | ||||
| // This type works similarly to math/big.Int, and all arguments and receivers | ||||
| // are allowed to alias. | ||||
| // | ||||
| // The zero value is NOT valid, and it may be used only as a receiver. | ||||
| type Point struct { | ||||
| 	// Make the type not comparable (i.e. used with == or as a map key), as | ||||
| 	// equivalent points can be represented by different Go values. | ||||
| 	_ incomparable | ||||
|  | ||||
| 	// The point is internally represented in extended coordinates (X, Y, Z, T) | ||||
| 	// where x = X/Z, y = Y/Z, and xy = T/Z per https://eprint.iacr.org/2008/522. | ||||
| 	x, y, z, t field.Element | ||||
| } | ||||
|  | ||||
| type incomparable [0]func() | ||||
|  | ||||
| func checkInitialized(points ...*Point) { | ||||
| 	for _, p := range points { | ||||
| 		if p.x == (field.Element{}) && p.y == (field.Element{}) { | ||||
| 			panic("edwards25519: use of uninitialized Point") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type projCached struct { | ||||
| 	YplusX, YminusX, Z, T2d field.Element | ||||
| } | ||||
|  | ||||
| type affineCached struct { | ||||
| 	YplusX, YminusX, T2d field.Element | ||||
| } | ||||
|  | ||||
| // Constructors. | ||||
|  | ||||
| func (v *projP2) Zero() *projP2 { | ||||
| 	v.X.Zero() | ||||
| 	v.Y.One() | ||||
| 	v.Z.One() | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // identity is the point at infinity. | ||||
| var identity, _ = new(Point).SetBytes([]byte{ | ||||
| 	1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||||
| 	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) | ||||
|  | ||||
| // NewIdentityPoint returns a new Point set to the identity. | ||||
| func NewIdentityPoint() *Point { | ||||
| 	return new(Point).Set(identity) | ||||
| } | ||||
|  | ||||
| // generator is the canonical curve basepoint. See TestGenerator for the | ||||
| // correspondence of this encoding with the values in RFC 8032. | ||||
| var generator, _ = new(Point).SetBytes([]byte{ | ||||
| 	0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, | ||||
| 	0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, | ||||
| 	0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, | ||||
| 	0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66}) | ||||
|  | ||||
| // NewGeneratorPoint returns a new Point set to the canonical generator. | ||||
| func NewGeneratorPoint() *Point { | ||||
| 	return new(Point).Set(generator) | ||||
| } | ||||
|  | ||||
| func (v *projCached) Zero() *projCached { | ||||
| 	v.YplusX.One() | ||||
| 	v.YminusX.One() | ||||
| 	v.Z.One() | ||||
| 	v.T2d.Zero() | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| func (v *affineCached) Zero() *affineCached { | ||||
| 	v.YplusX.One() | ||||
| 	v.YminusX.One() | ||||
| 	v.T2d.Zero() | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // Assignments. | ||||
|  | ||||
| // Set sets v = u, and returns v. | ||||
| func (v *Point) Set(u *Point) *Point { | ||||
| 	*v = *u | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // Encoding. | ||||
|  | ||||
| // Bytes returns the canonical 32-byte encoding of v, according to RFC 8032, | ||||
| // Section 5.1.2. | ||||
| func (v *Point) Bytes() []byte { | ||||
| 	// This function is outlined to make the allocations inline in the caller | ||||
| 	// rather than happen on the heap. | ||||
| 	var buf [32]byte | ||||
| 	return v.bytes(&buf) | ||||
| } | ||||
|  | ||||
| func (v *Point) bytes(buf *[32]byte) []byte { | ||||
| 	checkInitialized(v) | ||||
|  | ||||
| 	var zInv, x, y field.Element | ||||
| 	zInv.Invert(&v.z)       // zInv = 1 / Z | ||||
| 	x.Multiply(&v.x, &zInv) // x = X / Z | ||||
| 	y.Multiply(&v.y, &zInv) // y = Y / Z | ||||
|  | ||||
| 	out := copyFieldElement(buf, &y) | ||||
| 	out[31] |= byte(x.IsNegative() << 7) | ||||
| 	return out | ||||
| } | ||||
|  | ||||
| var feOne = new(field.Element).One() | ||||
|  | ||||
| // SetBytes sets v = x, where x is a 32-byte encoding of v. If x does not | ||||
| // represent a valid point on the curve, SetBytes returns nil and an error and | ||||
| // the receiver is unchanged. Otherwise, SetBytes returns v. | ||||
| // | ||||
| // Note that SetBytes accepts all non-canonical encodings of valid points. | ||||
| // That is, it follows decoding rules that match most implementations in | ||||
| // the ecosystem rather than RFC 8032. | ||||
| func (v *Point) SetBytes(x []byte) (*Point, error) { | ||||
| 	// Specifically, the non-canonical encodings that are accepted are | ||||
| 	//   1) the ones where the field element is not reduced (see the | ||||
| 	//      (*field.Element).SetBytes docs) and | ||||
| 	//   2) the ones where the x-coordinate is zero and the sign bit is set. | ||||
| 	// | ||||
| 	// Read more at https://hdevalence.ca/blog/2020-10-04-its-25519am, | ||||
| 	// specifically the "Canonical A, R" section. | ||||
|  | ||||
| 	y, err := new(field.Element).SetBytes(x) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.New("edwards25519: invalid point encoding length") | ||||
| 	} | ||||
|  | ||||
| 	// -x² + y² = 1 + dx²y² | ||||
| 	// x² + dx²y² = x²(dy² + 1) = y² - 1 | ||||
| 	// x² = (y² - 1) / (dy² + 1) | ||||
|  | ||||
| 	// u = y² - 1 | ||||
| 	y2 := new(field.Element).Square(y) | ||||
| 	u := new(field.Element).Subtract(y2, feOne) | ||||
|  | ||||
| 	// v = dy² + 1 | ||||
| 	vv := new(field.Element).Multiply(y2, d) | ||||
| 	vv = vv.Add(vv, feOne) | ||||
|  | ||||
| 	// x = +√(u/v) | ||||
| 	xx, wasSquare := new(field.Element).SqrtRatio(u, vv) | ||||
| 	if wasSquare == 0 { | ||||
| 		return nil, errors.New("edwards25519: invalid point encoding") | ||||
| 	} | ||||
|  | ||||
| 	// Select the negative square root if the sign bit is set. | ||||
| 	xxNeg := new(field.Element).Negate(xx) | ||||
| 	xx = xx.Select(xxNeg, xx, int(x[31]>>7)) | ||||
|  | ||||
| 	v.x.Set(xx) | ||||
| 	v.y.Set(y) | ||||
| 	v.z.One() | ||||
| 	v.t.Multiply(xx, y) // xy = T / Z | ||||
|  | ||||
| 	return v, nil | ||||
| } | ||||
|  | ||||
| func copyFieldElement(buf *[32]byte, v *field.Element) []byte { | ||||
| 	copy(buf[:], v.Bytes()) | ||||
| 	return buf[:] | ||||
| } | ||||
|  | ||||
| // Conversions. | ||||
|  | ||||
| func (v *projP2) FromP1xP1(p *projP1xP1) *projP2 { | ||||
| 	v.X.Multiply(&p.X, &p.T) | ||||
| 	v.Y.Multiply(&p.Y, &p.Z) | ||||
| 	v.Z.Multiply(&p.Z, &p.T) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| func (v *projP2) FromP3(p *Point) *projP2 { | ||||
| 	v.X.Set(&p.x) | ||||
| 	v.Y.Set(&p.y) | ||||
| 	v.Z.Set(&p.z) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| func (v *Point) fromP1xP1(p *projP1xP1) *Point { | ||||
| 	v.x.Multiply(&p.X, &p.T) | ||||
| 	v.y.Multiply(&p.Y, &p.Z) | ||||
| 	v.z.Multiply(&p.Z, &p.T) | ||||
| 	v.t.Multiply(&p.X, &p.Y) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| func (v *Point) fromP2(p *projP2) *Point { | ||||
| 	v.x.Multiply(&p.X, &p.Z) | ||||
| 	v.y.Multiply(&p.Y, &p.Z) | ||||
| 	v.z.Square(&p.Z) | ||||
| 	v.t.Multiply(&p.X, &p.Y) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // d is a constant in the curve equation. | ||||
| var d, _ = new(field.Element).SetBytes([]byte{ | ||||
| 	0xa3, 0x78, 0x59, 0x13, 0xca, 0x4d, 0xeb, 0x75, | ||||
| 	0xab, 0xd8, 0x41, 0x41, 0x4d, 0x0a, 0x70, 0x00, | ||||
| 	0x98, 0xe8, 0x79, 0x77, 0x79, 0x40, 0xc7, 0x8c, | ||||
| 	0x73, 0xfe, 0x6f, 0x2b, 0xee, 0x6c, 0x03, 0x52}) | ||||
| var d2 = new(field.Element).Add(d, d) | ||||
|  | ||||
| func (v *projCached) FromP3(p *Point) *projCached { | ||||
| 	v.YplusX.Add(&p.y, &p.x) | ||||
| 	v.YminusX.Subtract(&p.y, &p.x) | ||||
| 	v.Z.Set(&p.z) | ||||
| 	v.T2d.Multiply(&p.t, d2) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| func (v *affineCached) FromP3(p *Point) *affineCached { | ||||
| 	v.YplusX.Add(&p.y, &p.x) | ||||
| 	v.YminusX.Subtract(&p.y, &p.x) | ||||
| 	v.T2d.Multiply(&p.t, d2) | ||||
|  | ||||
| 	var invZ field.Element | ||||
| 	invZ.Invert(&p.z) | ||||
| 	v.YplusX.Multiply(&v.YplusX, &invZ) | ||||
| 	v.YminusX.Multiply(&v.YminusX, &invZ) | ||||
| 	v.T2d.Multiply(&v.T2d, &invZ) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // (Re)addition and subtraction. | ||||
|  | ||||
| // Add sets v = p + q, and returns v. | ||||
| func (v *Point) Add(p, q *Point) *Point { | ||||
| 	checkInitialized(p, q) | ||||
| 	qCached := new(projCached).FromP3(q) | ||||
| 	result := new(projP1xP1).Add(p, qCached) | ||||
| 	return v.fromP1xP1(result) | ||||
| } | ||||
|  | ||||
| // Subtract sets v = p - q, and returns v. | ||||
| func (v *Point) Subtract(p, q *Point) *Point { | ||||
| 	checkInitialized(p, q) | ||||
| 	qCached := new(projCached).FromP3(q) | ||||
| 	result := new(projP1xP1).Sub(p, qCached) | ||||
| 	return v.fromP1xP1(result) | ||||
| } | ||||
|  | ||||
| func (v *projP1xP1) Add(p *Point, q *projCached) *projP1xP1 { | ||||
| 	var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element | ||||
|  | ||||
| 	YplusX.Add(&p.y, &p.x) | ||||
| 	YminusX.Subtract(&p.y, &p.x) | ||||
|  | ||||
| 	PP.Multiply(&YplusX, &q.YplusX) | ||||
| 	MM.Multiply(&YminusX, &q.YminusX) | ||||
| 	TT2d.Multiply(&p.t, &q.T2d) | ||||
| 	ZZ2.Multiply(&p.z, &q.Z) | ||||
|  | ||||
| 	ZZ2.Add(&ZZ2, &ZZ2) | ||||
|  | ||||
| 	v.X.Subtract(&PP, &MM) | ||||
| 	v.Y.Add(&PP, &MM) | ||||
| 	v.Z.Add(&ZZ2, &TT2d) | ||||
| 	v.T.Subtract(&ZZ2, &TT2d) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| func (v *projP1xP1) Sub(p *Point, q *projCached) *projP1xP1 { | ||||
| 	var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element | ||||
|  | ||||
| 	YplusX.Add(&p.y, &p.x) | ||||
| 	YminusX.Subtract(&p.y, &p.x) | ||||
|  | ||||
| 	PP.Multiply(&YplusX, &q.YminusX) // flipped sign | ||||
| 	MM.Multiply(&YminusX, &q.YplusX) // flipped sign | ||||
| 	TT2d.Multiply(&p.t, &q.T2d) | ||||
| 	ZZ2.Multiply(&p.z, &q.Z) | ||||
|  | ||||
| 	ZZ2.Add(&ZZ2, &ZZ2) | ||||
|  | ||||
| 	v.X.Subtract(&PP, &MM) | ||||
| 	v.Y.Add(&PP, &MM) | ||||
| 	v.Z.Subtract(&ZZ2, &TT2d) // flipped sign | ||||
| 	v.T.Add(&ZZ2, &TT2d)      // flipped sign | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| func (v *projP1xP1) AddAffine(p *Point, q *affineCached) *projP1xP1 { | ||||
| 	var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element | ||||
|  | ||||
| 	YplusX.Add(&p.y, &p.x) | ||||
| 	YminusX.Subtract(&p.y, &p.x) | ||||
|  | ||||
| 	PP.Multiply(&YplusX, &q.YplusX) | ||||
| 	MM.Multiply(&YminusX, &q.YminusX) | ||||
| 	TT2d.Multiply(&p.t, &q.T2d) | ||||
|  | ||||
| 	Z2.Add(&p.z, &p.z) | ||||
|  | ||||
| 	v.X.Subtract(&PP, &MM) | ||||
| 	v.Y.Add(&PP, &MM) | ||||
| 	v.Z.Add(&Z2, &TT2d) | ||||
| 	v.T.Subtract(&Z2, &TT2d) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| func (v *projP1xP1) SubAffine(p *Point, q *affineCached) *projP1xP1 { | ||||
| 	var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element | ||||
|  | ||||
| 	YplusX.Add(&p.y, &p.x) | ||||
| 	YminusX.Subtract(&p.y, &p.x) | ||||
|  | ||||
| 	PP.Multiply(&YplusX, &q.YminusX) // flipped sign | ||||
| 	MM.Multiply(&YminusX, &q.YplusX) // flipped sign | ||||
| 	TT2d.Multiply(&p.t, &q.T2d) | ||||
|  | ||||
| 	Z2.Add(&p.z, &p.z) | ||||
|  | ||||
| 	v.X.Subtract(&PP, &MM) | ||||
| 	v.Y.Add(&PP, &MM) | ||||
| 	v.Z.Subtract(&Z2, &TT2d) // flipped sign | ||||
| 	v.T.Add(&Z2, &TT2d)      // flipped sign | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // Doubling. | ||||
|  | ||||
| func (v *projP1xP1) Double(p *projP2) *projP1xP1 { | ||||
| 	var XX, YY, ZZ2, XplusYsq field.Element | ||||
|  | ||||
| 	XX.Square(&p.X) | ||||
| 	YY.Square(&p.Y) | ||||
| 	ZZ2.Square(&p.Z) | ||||
| 	ZZ2.Add(&ZZ2, &ZZ2) | ||||
| 	XplusYsq.Add(&p.X, &p.Y) | ||||
| 	XplusYsq.Square(&XplusYsq) | ||||
|  | ||||
| 	v.Y.Add(&YY, &XX) | ||||
| 	v.Z.Subtract(&YY, &XX) | ||||
|  | ||||
| 	v.X.Subtract(&XplusYsq, &v.Y) | ||||
| 	v.T.Subtract(&ZZ2, &v.Z) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // Negation. | ||||
|  | ||||
| // Negate sets v = -p, and returns v. | ||||
| func (v *Point) Negate(p *Point) *Point { | ||||
| 	checkInitialized(p) | ||||
| 	v.x.Negate(&p.x) | ||||
| 	v.y.Set(&p.y) | ||||
| 	v.z.Set(&p.z) | ||||
| 	v.t.Negate(&p.t) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // Equal returns 1 if v is equivalent to u, and 0 otherwise. | ||||
| func (v *Point) Equal(u *Point) int { | ||||
| 	checkInitialized(v, u) | ||||
|  | ||||
| 	var t1, t2, t3, t4 field.Element | ||||
| 	t1.Multiply(&v.x, &u.z) | ||||
| 	t2.Multiply(&u.x, &v.z) | ||||
| 	t3.Multiply(&v.y, &u.z) | ||||
| 	t4.Multiply(&u.y, &v.z) | ||||
|  | ||||
| 	return t1.Equal(&t2) & t3.Equal(&t4) | ||||
| } | ||||
|  | ||||
| // Constant-time operations | ||||
|  | ||||
| // Select sets v to a if cond == 1 and to b if cond == 0. | ||||
| func (v *projCached) Select(a, b *projCached, cond int) *projCached { | ||||
| 	v.YplusX.Select(&a.YplusX, &b.YplusX, cond) | ||||
| 	v.YminusX.Select(&a.YminusX, &b.YminusX, cond) | ||||
| 	v.Z.Select(&a.Z, &b.Z, cond) | ||||
| 	v.T2d.Select(&a.T2d, &b.T2d, cond) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // Select sets v to a if cond == 1 and to b if cond == 0. | ||||
| func (v *affineCached) Select(a, b *affineCached, cond int) *affineCached { | ||||
| 	v.YplusX.Select(&a.YplusX, &b.YplusX, cond) | ||||
| 	v.YminusX.Select(&a.YminusX, &b.YminusX, cond) | ||||
| 	v.T2d.Select(&a.T2d, &b.T2d, cond) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0. | ||||
| func (v *projCached) CondNeg(cond int) *projCached { | ||||
| 	v.YplusX.Swap(&v.YminusX, cond) | ||||
| 	v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0. | ||||
| func (v *affineCached) CondNeg(cond int) *affineCached { | ||||
| 	v.YplusX.Swap(&v.YminusX, cond) | ||||
| 	v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond) | ||||
| 	return v | ||||
| } | ||||
							
								
								
									
										349
									
								
								vendor/filippo.io/edwards25519/extra.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,349 @@ | ||||
| // Copyright (c) 2021 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package edwards25519 | ||||
|  | ||||
| // This file contains additional functionality that is not included in the | ||||
| // upstream crypto/internal/edwards25519 package. | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
|  | ||||
| 	"filippo.io/edwards25519/field" | ||||
| ) | ||||
|  | ||||
| // ExtendedCoordinates returns v in extended coordinates (X:Y:Z:T) where | ||||
| // x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522. | ||||
| func (v *Point) ExtendedCoordinates() (X, Y, Z, T *field.Element) { | ||||
| 	// This function is outlined to make the allocations inline in the caller | ||||
| 	// rather than happen on the heap. Don't change the style without making | ||||
| 	// sure it doesn't increase the inliner cost. | ||||
| 	var e [4]field.Element | ||||
| 	X, Y, Z, T = v.extendedCoordinates(&e) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (v *Point) extendedCoordinates(e *[4]field.Element) (X, Y, Z, T *field.Element) { | ||||
| 	checkInitialized(v) | ||||
| 	X = e[0].Set(&v.x) | ||||
| 	Y = e[1].Set(&v.y) | ||||
| 	Z = e[2].Set(&v.z) | ||||
| 	T = e[3].Set(&v.t) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // SetExtendedCoordinates sets v = (X:Y:Z:T) in extended coordinates where | ||||
| // x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522. | ||||
| // | ||||
| // If the coordinates are invalid or don't represent a valid point on the curve, | ||||
| // SetExtendedCoordinates returns nil and an error and the receiver is | ||||
| // unchanged. Otherwise, SetExtendedCoordinates returns v. | ||||
| func (v *Point) SetExtendedCoordinates(X, Y, Z, T *field.Element) (*Point, error) { | ||||
| 	if !isOnCurve(X, Y, Z, T) { | ||||
| 		return nil, errors.New("edwards25519: invalid point coordinates") | ||||
| 	} | ||||
| 	v.x.Set(X) | ||||
| 	v.y.Set(Y) | ||||
| 	v.z.Set(Z) | ||||
| 	v.t.Set(T) | ||||
| 	return v, nil | ||||
| } | ||||
|  | ||||
| func isOnCurve(X, Y, Z, T *field.Element) bool { | ||||
| 	var lhs, rhs field.Element | ||||
| 	XX := new(field.Element).Square(X) | ||||
| 	YY := new(field.Element).Square(Y) | ||||
| 	ZZ := new(field.Element).Square(Z) | ||||
| 	TT := new(field.Element).Square(T) | ||||
| 	// -x² + y² = 1 + dx²y² | ||||
| 	// -(X/Z)² + (Y/Z)² = 1 + d(T/Z)² | ||||
| 	// -X² + Y² = Z² + dT² | ||||
| 	lhs.Subtract(YY, XX) | ||||
| 	rhs.Multiply(d, TT).Add(&rhs, ZZ) | ||||
| 	if lhs.Equal(&rhs) != 1 { | ||||
| 		return false | ||||
| 	} | ||||
| 	// xy = T/Z | ||||
| 	// XY/Z² = T/Z | ||||
| 	// XY = TZ | ||||
| 	lhs.Multiply(X, Y) | ||||
| 	rhs.Multiply(T, Z) | ||||
| 	return lhs.Equal(&rhs) == 1 | ||||
| } | ||||
|  | ||||
| // BytesMontgomery converts v to a point on the birationally-equivalent | ||||
| // Curve25519 Montgomery curve, and returns its canonical 32 bytes encoding | ||||
| // according to RFC 7748. | ||||
| // | ||||
| // Note that BytesMontgomery only encodes the u-coordinate, so v and -v encode | ||||
| // to the same value. If v is the identity point, BytesMontgomery returns 32 | ||||
| // zero bytes, analogously to the X25519 function. | ||||
| // | ||||
| // The lack of an inverse operation (such as SetMontgomeryBytes) is deliberate: | ||||
| // while every valid edwards25519 point has a unique u-coordinate Montgomery | ||||
| // encoding, X25519 accepts inputs on the quadratic twist, which don't correspond | ||||
| // to any edwards25519 point, and every other X25519 input corresponds to two | ||||
| // edwards25519 points. | ||||
| func (v *Point) BytesMontgomery() []byte { | ||||
| 	// This function is outlined to make the allocations inline in the caller | ||||
| 	// rather than happen on the heap. | ||||
| 	var buf [32]byte | ||||
| 	return v.bytesMontgomery(&buf) | ||||
| } | ||||
|  | ||||
| func (v *Point) bytesMontgomery(buf *[32]byte) []byte { | ||||
| 	checkInitialized(v) | ||||
|  | ||||
| 	// RFC 7748, Section 4.1 provides the bilinear map to calculate the | ||||
| 	// Montgomery u-coordinate | ||||
| 	// | ||||
| 	//              u = (1 + y) / (1 - y) | ||||
| 	// | ||||
| 	// where y = Y / Z. | ||||
|  | ||||
| 	var y, recip, u field.Element | ||||
|  | ||||
| 	y.Multiply(&v.y, y.Invert(&v.z))        // y = Y / Z | ||||
| 	recip.Invert(recip.Subtract(feOne, &y)) // r = 1/(1 - y) | ||||
| 	u.Multiply(u.Add(feOne, &y), &recip)    // u = (1 + y)*r | ||||
|  | ||||
| 	return copyFieldElement(buf, &u) | ||||
| } | ||||
|  | ||||
| // MultByCofactor sets v = 8 * p, and returns v. | ||||
| func (v *Point) MultByCofactor(p *Point) *Point { | ||||
| 	checkInitialized(p) | ||||
| 	result := projP1xP1{} | ||||
| 	pp := (&projP2{}).FromP3(p) | ||||
| 	result.Double(pp) | ||||
| 	pp.FromP1xP1(&result) | ||||
| 	result.Double(pp) | ||||
| 	pp.FromP1xP1(&result) | ||||
| 	result.Double(pp) | ||||
| 	return v.fromP1xP1(&result) | ||||
| } | ||||
|  | ||||
| // Given k > 0, set s = s**(2*i). | ||||
| func (s *Scalar) pow2k(k int) { | ||||
| 	for i := 0; i < k; i++ { | ||||
| 		s.Multiply(s, s) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Invert sets s to the inverse of a nonzero scalar v, and returns s. | ||||
| // | ||||
| // If t is zero, Invert returns zero. | ||||
| func (s *Scalar) Invert(t *Scalar) *Scalar { | ||||
| 	// Uses a hardcoded sliding window of width 4. | ||||
| 	var table [8]Scalar | ||||
| 	var tt Scalar | ||||
| 	tt.Multiply(t, t) | ||||
| 	table[0] = *t | ||||
| 	for i := 0; i < 7; i++ { | ||||
| 		table[i+1].Multiply(&table[i], &tt) | ||||
| 	} | ||||
| 	// Now table = [t**1, t**3, t**5, t**7, t**9, t**11, t**13, t**15] | ||||
| 	// so t**k = t[k/2] for odd k | ||||
|  | ||||
| 	// To compute the sliding window digits, use the following Sage script: | ||||
|  | ||||
| 	// sage: import itertools | ||||
| 	// sage: def sliding_window(w,k): | ||||
| 	// ....:     digits = [] | ||||
| 	// ....:     while k > 0: | ||||
| 	// ....:         if k % 2 == 1: | ||||
| 	// ....:             kmod = k % (2**w) | ||||
| 	// ....:             digits.append(kmod) | ||||
| 	// ....:             k = k - kmod | ||||
| 	// ....:         else: | ||||
| 	// ....:             digits.append(0) | ||||
| 	// ....:         k = k // 2 | ||||
| 	// ....:     return digits | ||||
|  | ||||
| 	// Now we can compute s roughly as follows: | ||||
|  | ||||
| 	// sage: s = 1 | ||||
| 	// sage: for coeff in reversed(sliding_window(4,l-2)): | ||||
| 	// ....:     s = s*s | ||||
| 	// ....:     if coeff > 0 : | ||||
| 	// ....:         s = s*t**coeff | ||||
|  | ||||
| 	// This works on one bit at a time, with many runs of zeros. | ||||
| 	// The digits can be collapsed into [(count, coeff)] as follows: | ||||
|  | ||||
| 	// sage: [(len(list(group)),d) for d,group in itertools.groupby(sliding_window(4,l-2))] | ||||
|  | ||||
| 	// Entries of the form (k, 0) turn into pow2k(k) | ||||
| 	// Entries of the form (1, coeff) turn into a squaring and then a table lookup. | ||||
| 	// We can fold the squaring into the previous pow2k(k) as pow2k(k+1). | ||||
|  | ||||
| 	*s = table[1/2] | ||||
| 	s.pow2k(127 + 1) | ||||
| 	s.Multiply(s, &table[1/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[9/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[11/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[13/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[15/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[7/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[15/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[5/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[1/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[15/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[15/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[7/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[3/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[11/2]) | ||||
| 	s.pow2k(5 + 1) | ||||
| 	s.Multiply(s, &table[11/2]) | ||||
| 	s.pow2k(9 + 1) | ||||
| 	s.Multiply(s, &table[9/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[3/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[3/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[3/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[9/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[7/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[3/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[13/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[7/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[9/2]) | ||||
| 	s.pow2k(3 + 1) | ||||
| 	s.Multiply(s, &table[15/2]) | ||||
| 	s.pow2k(4 + 1) | ||||
| 	s.Multiply(s, &table[11/2]) | ||||
|  | ||||
| 	return s | ||||
| } | ||||
|  | ||||
| // MultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v. | ||||
| // | ||||
| // Execution time depends only on the lengths of the two slices, which must match. | ||||
| func (v *Point) MultiScalarMult(scalars []*Scalar, points []*Point) *Point { | ||||
| 	if len(scalars) != len(points) { | ||||
| 		panic("edwards25519: called MultiScalarMult with different size inputs") | ||||
| 	} | ||||
| 	checkInitialized(points...) | ||||
|  | ||||
| 	// Proceed as in the single-base case, but share doublings | ||||
| 	// between each point in the multiscalar equation. | ||||
|  | ||||
| 	// Build lookup tables for each point | ||||
| 	tables := make([]projLookupTable, len(points)) | ||||
| 	for i := range tables { | ||||
| 		tables[i].FromP3(points[i]) | ||||
| 	} | ||||
| 	// Compute signed radix-16 digits for each scalar | ||||
| 	digits := make([][64]int8, len(scalars)) | ||||
| 	for i := range digits { | ||||
| 		digits[i] = scalars[i].signedRadix16() | ||||
| 	} | ||||
|  | ||||
| 	// Unwrap first loop iteration to save computing 16*identity | ||||
| 	multiple := &projCached{} | ||||
| 	tmp1 := &projP1xP1{} | ||||
| 	tmp2 := &projP2{} | ||||
| 	// Lookup-and-add the appropriate multiple of each input point | ||||
| 	for j := range tables { | ||||
| 		tables[j].SelectInto(multiple, digits[j][63]) | ||||
| 		tmp1.Add(v, multiple) // tmp1 = v + x_(j,63)*Q in P1xP1 coords | ||||
| 		v.fromP1xP1(tmp1)     // update v | ||||
| 	} | ||||
| 	tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration | ||||
| 	for i := 62; i >= 0; i-- { | ||||
| 		tmp1.Double(tmp2)    // tmp1 =  2*(prev) in P1xP1 coords | ||||
| 		tmp2.FromP1xP1(tmp1) // tmp2 =  2*(prev) in P2 coords | ||||
| 		tmp1.Double(tmp2)    // tmp1 =  4*(prev) in P1xP1 coords | ||||
| 		tmp2.FromP1xP1(tmp1) // tmp2 =  4*(prev) in P2 coords | ||||
| 		tmp1.Double(tmp2)    // tmp1 =  8*(prev) in P1xP1 coords | ||||
| 		tmp2.FromP1xP1(tmp1) // tmp2 =  8*(prev) in P2 coords | ||||
| 		tmp1.Double(tmp2)    // tmp1 = 16*(prev) in P1xP1 coords | ||||
| 		v.fromP1xP1(tmp1)    //    v = 16*(prev) in P3 coords | ||||
| 		// Lookup-and-add the appropriate multiple of each input point | ||||
| 		for j := range tables { | ||||
| 			tables[j].SelectInto(multiple, digits[j][i]) | ||||
| 			tmp1.Add(v, multiple) // tmp1 = v + x_(j,i)*Q in P1xP1 coords | ||||
| 			v.fromP1xP1(tmp1)     // update v | ||||
| 		} | ||||
| 		tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration | ||||
| 	} | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // VarTimeMultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v. | ||||
| // | ||||
| // Execution time depends on the inputs. | ||||
| func (v *Point) VarTimeMultiScalarMult(scalars []*Scalar, points []*Point) *Point { | ||||
| 	if len(scalars) != len(points) { | ||||
| 		panic("edwards25519: called VarTimeMultiScalarMult with different size inputs") | ||||
| 	} | ||||
| 	checkInitialized(points...) | ||||
|  | ||||
| 	// Generalize double-base NAF computation to arbitrary sizes. | ||||
| 	// Here all the points are dynamic, so we only use the smaller | ||||
| 	// tables. | ||||
|  | ||||
| 	// Build lookup tables for each point | ||||
| 	tables := make([]nafLookupTable5, len(points)) | ||||
| 	for i := range tables { | ||||
| 		tables[i].FromP3(points[i]) | ||||
| 	} | ||||
| 	// Compute a NAF for each scalar | ||||
| 	nafs := make([][256]int8, len(scalars)) | ||||
| 	for i := range nafs { | ||||
| 		nafs[i] = scalars[i].nonAdjacentForm(5) | ||||
| 	} | ||||
|  | ||||
| 	multiple := &projCached{} | ||||
| 	tmp1 := &projP1xP1{} | ||||
| 	tmp2 := &projP2{} | ||||
| 	tmp2.Zero() | ||||
|  | ||||
| 	// Move from high to low bits, doubling the accumulator | ||||
| 	// at each iteration and checking whether there is a nonzero | ||||
| 	// coefficient to look up a multiple of. | ||||
| 	// | ||||
| 	// Skip trying to find the first nonzero coefficent, because | ||||
| 	// searching might be more work than a few extra doublings. | ||||
| 	for i := 255; i >= 0; i-- { | ||||
| 		tmp1.Double(tmp2) | ||||
|  | ||||
| 		for j := range nafs { | ||||
| 			if nafs[j][i] > 0 { | ||||
| 				v.fromP1xP1(tmp1) | ||||
| 				tables[j].SelectInto(multiple, nafs[j][i]) | ||||
| 				tmp1.Add(v, multiple) | ||||
| 			} else if nafs[j][i] < 0 { | ||||
| 				v.fromP1xP1(tmp1) | ||||
| 				tables[j].SelectInto(multiple, -nafs[j][i]) | ||||
| 				tmp1.Sub(v, multiple) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		tmp2.FromP1xP1(tmp1) | ||||
| 	} | ||||
|  | ||||
| 	v.fromP2(tmp2) | ||||
| 	return v | ||||
| } | ||||
							
								
								
									
										420
									
								
								vendor/filippo.io/edwards25519/field/fe.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,420 @@ | ||||
| // Copyright (c) 2017 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| // Package field implements fast arithmetic modulo 2^255-19. | ||||
| package field | ||||
|  | ||||
| import ( | ||||
| 	"crypto/subtle" | ||||
| 	"encoding/binary" | ||||
| 	"errors" | ||||
| 	"math/bits" | ||||
| ) | ||||
|  | ||||
| // Element represents an element of the field GF(2^255-19). Note that this | ||||
| // is not a cryptographically secure group, and should only be used to interact | ||||
| // with edwards25519.Point coordinates. | ||||
| // | ||||
| // This type works similarly to math/big.Int, and all arguments and receivers | ||||
| // are allowed to alias. | ||||
| // | ||||
| // The zero value is a valid zero element. | ||||
| type Element struct { | ||||
| 	// An element t represents the integer | ||||
| 	//     t.l0 + t.l1*2^51 + t.l2*2^102 + t.l3*2^153 + t.l4*2^204 | ||||
| 	// | ||||
| 	// Between operations, all limbs are expected to be lower than 2^52. | ||||
| 	l0 uint64 | ||||
| 	l1 uint64 | ||||
| 	l2 uint64 | ||||
| 	l3 uint64 | ||||
| 	l4 uint64 | ||||
| } | ||||
|  | ||||
| const maskLow51Bits uint64 = (1 << 51) - 1 | ||||
|  | ||||
| var feZero = &Element{0, 0, 0, 0, 0} | ||||
|  | ||||
| // Zero sets v = 0, and returns v. | ||||
| func (v *Element) Zero() *Element { | ||||
| 	*v = *feZero | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| var feOne = &Element{1, 0, 0, 0, 0} | ||||
|  | ||||
| // One sets v = 1, and returns v. | ||||
| func (v *Element) One() *Element { | ||||
| 	*v = *feOne | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // reduce reduces v modulo 2^255 - 19 and returns it. | ||||
| func (v *Element) reduce() *Element { | ||||
| 	v.carryPropagate() | ||||
|  | ||||
| 	// After the light reduction we now have a field element representation | ||||
| 	// v < 2^255 + 2^13 * 19, but need v < 2^255 - 19. | ||||
|  | ||||
| 	// If v >= 2^255 - 19, then v + 19 >= 2^255, which would overflow 2^255 - 1, | ||||
| 	// generating a carry. That is, c will be 0 if v < 2^255 - 19, and 1 otherwise. | ||||
| 	c := (v.l0 + 19) >> 51 | ||||
| 	c = (v.l1 + c) >> 51 | ||||
| 	c = (v.l2 + c) >> 51 | ||||
| 	c = (v.l3 + c) >> 51 | ||||
| 	c = (v.l4 + c) >> 51 | ||||
|  | ||||
| 	// If v < 2^255 - 19 and c = 0, this will be a no-op. Otherwise, it's | ||||
| 	// effectively applying the reduction identity to the carry. | ||||
| 	v.l0 += 19 * c | ||||
|  | ||||
| 	v.l1 += v.l0 >> 51 | ||||
| 	v.l0 = v.l0 & maskLow51Bits | ||||
| 	v.l2 += v.l1 >> 51 | ||||
| 	v.l1 = v.l1 & maskLow51Bits | ||||
| 	v.l3 += v.l2 >> 51 | ||||
| 	v.l2 = v.l2 & maskLow51Bits | ||||
| 	v.l4 += v.l3 >> 51 | ||||
| 	v.l3 = v.l3 & maskLow51Bits | ||||
| 	// no additional carry | ||||
| 	v.l4 = v.l4 & maskLow51Bits | ||||
|  | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // Add sets v = a + b, and returns v. | ||||
| func (v *Element) Add(a, b *Element) *Element { | ||||
| 	v.l0 = a.l0 + b.l0 | ||||
| 	v.l1 = a.l1 + b.l1 | ||||
| 	v.l2 = a.l2 + b.l2 | ||||
| 	v.l3 = a.l3 + b.l3 | ||||
| 	v.l4 = a.l4 + b.l4 | ||||
| 	// Using the generic implementation here is actually faster than the | ||||
| 	// assembly. Probably because the body of this function is so simple that | ||||
| 	// the compiler can figure out better optimizations by inlining the carry | ||||
| 	// propagation. | ||||
| 	return v.carryPropagateGeneric() | ||||
| } | ||||
|  | ||||
| // Subtract sets v = a - b, and returns v. | ||||
| func (v *Element) Subtract(a, b *Element) *Element { | ||||
| 	// We first add 2 * p, to guarantee the subtraction won't underflow, and | ||||
| 	// then subtract b (which can be up to 2^255 + 2^13 * 19). | ||||
| 	v.l0 = (a.l0 + 0xFFFFFFFFFFFDA) - b.l0 | ||||
| 	v.l1 = (a.l1 + 0xFFFFFFFFFFFFE) - b.l1 | ||||
| 	v.l2 = (a.l2 + 0xFFFFFFFFFFFFE) - b.l2 | ||||
| 	v.l3 = (a.l3 + 0xFFFFFFFFFFFFE) - b.l3 | ||||
| 	v.l4 = (a.l4 + 0xFFFFFFFFFFFFE) - b.l4 | ||||
| 	return v.carryPropagate() | ||||
| } | ||||
|  | ||||
| // Negate sets v = -a, and returns v. | ||||
| func (v *Element) Negate(a *Element) *Element { | ||||
| 	return v.Subtract(feZero, a) | ||||
| } | ||||
|  | ||||
| // Invert sets v = 1/z mod p, and returns v. | ||||
| // | ||||
| // If z == 0, Invert returns v = 0. | ||||
| func (v *Element) Invert(z *Element) *Element { | ||||
| 	// Inversion is implemented as exponentiation with exponent p − 2. It uses the | ||||
| 	// same sequence of 255 squarings and 11 multiplications as [Curve25519]. | ||||
| 	var z2, z9, z11, z2_5_0, z2_10_0, z2_20_0, z2_50_0, z2_100_0, t Element | ||||
|  | ||||
| 	z2.Square(z)             // 2 | ||||
| 	t.Square(&z2)            // 4 | ||||
| 	t.Square(&t)             // 8 | ||||
| 	z9.Multiply(&t, z)       // 9 | ||||
| 	z11.Multiply(&z9, &z2)   // 11 | ||||
| 	t.Square(&z11)           // 22 | ||||
| 	z2_5_0.Multiply(&t, &z9) // 31 = 2^5 - 2^0 | ||||
|  | ||||
| 	t.Square(&z2_5_0) // 2^6 - 2^1 | ||||
| 	for i := 0; i < 4; i++ { | ||||
| 		t.Square(&t) // 2^10 - 2^5 | ||||
| 	} | ||||
| 	z2_10_0.Multiply(&t, &z2_5_0) // 2^10 - 2^0 | ||||
|  | ||||
| 	t.Square(&z2_10_0) // 2^11 - 2^1 | ||||
| 	for i := 0; i < 9; i++ { | ||||
| 		t.Square(&t) // 2^20 - 2^10 | ||||
| 	} | ||||
| 	z2_20_0.Multiply(&t, &z2_10_0) // 2^20 - 2^0 | ||||
|  | ||||
| 	t.Square(&z2_20_0) // 2^21 - 2^1 | ||||
| 	for i := 0; i < 19; i++ { | ||||
| 		t.Square(&t) // 2^40 - 2^20 | ||||
| 	} | ||||
| 	t.Multiply(&t, &z2_20_0) // 2^40 - 2^0 | ||||
|  | ||||
| 	t.Square(&t) // 2^41 - 2^1 | ||||
| 	for i := 0; i < 9; i++ { | ||||
| 		t.Square(&t) // 2^50 - 2^10 | ||||
| 	} | ||||
| 	z2_50_0.Multiply(&t, &z2_10_0) // 2^50 - 2^0 | ||||
|  | ||||
| 	t.Square(&z2_50_0) // 2^51 - 2^1 | ||||
| 	for i := 0; i < 49; i++ { | ||||
| 		t.Square(&t) // 2^100 - 2^50 | ||||
| 	} | ||||
| 	z2_100_0.Multiply(&t, &z2_50_0) // 2^100 - 2^0 | ||||
|  | ||||
| 	t.Square(&z2_100_0) // 2^101 - 2^1 | ||||
| 	for i := 0; i < 99; i++ { | ||||
| 		t.Square(&t) // 2^200 - 2^100 | ||||
| 	} | ||||
| 	t.Multiply(&t, &z2_100_0) // 2^200 - 2^0 | ||||
|  | ||||
| 	t.Square(&t) // 2^201 - 2^1 | ||||
| 	for i := 0; i < 49; i++ { | ||||
| 		t.Square(&t) // 2^250 - 2^50 | ||||
| 	} | ||||
| 	t.Multiply(&t, &z2_50_0) // 2^250 - 2^0 | ||||
|  | ||||
| 	t.Square(&t) // 2^251 - 2^1 | ||||
| 	t.Square(&t) // 2^252 - 2^2 | ||||
| 	t.Square(&t) // 2^253 - 2^3 | ||||
| 	t.Square(&t) // 2^254 - 2^4 | ||||
| 	t.Square(&t) // 2^255 - 2^5 | ||||
|  | ||||
| 	return v.Multiply(&t, &z11) // 2^255 - 21 | ||||
| } | ||||
|  | ||||
| // Set sets v = a, and returns v. | ||||
| func (v *Element) Set(a *Element) *Element { | ||||
| 	*v = *a | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // SetBytes sets v to x, where x is a 32-byte little-endian encoding. If x is | ||||
| // not of the right length, SetBytes returns nil and an error, and the | ||||
| // receiver is unchanged. | ||||
| // | ||||
| // Consistent with RFC 7748, the most significant bit (the high bit of the | ||||
| // last byte) is ignored, and non-canonical values (2^255-19 through 2^255-1) | ||||
| // are accepted. Note that this is laxer than specified by RFC 8032, but | ||||
| // consistent with most Ed25519 implementations. | ||||
| func (v *Element) SetBytes(x []byte) (*Element, error) { | ||||
| 	if len(x) != 32 { | ||||
| 		return nil, errors.New("edwards25519: invalid field element input size") | ||||
| 	} | ||||
|  | ||||
| 	// Bits 0:51 (bytes 0:8, bits 0:64, shift 0, mask 51). | ||||
| 	v.l0 = binary.LittleEndian.Uint64(x[0:8]) | ||||
| 	v.l0 &= maskLow51Bits | ||||
| 	// Bits 51:102 (bytes 6:14, bits 48:112, shift 3, mask 51). | ||||
| 	v.l1 = binary.LittleEndian.Uint64(x[6:14]) >> 3 | ||||
| 	v.l1 &= maskLow51Bits | ||||
| 	// Bits 102:153 (bytes 12:20, bits 96:160, shift 6, mask 51). | ||||
| 	v.l2 = binary.LittleEndian.Uint64(x[12:20]) >> 6 | ||||
| 	v.l2 &= maskLow51Bits | ||||
| 	// Bits 153:204 (bytes 19:27, bits 152:216, shift 1, mask 51). | ||||
| 	v.l3 = binary.LittleEndian.Uint64(x[19:27]) >> 1 | ||||
| 	v.l3 &= maskLow51Bits | ||||
| 	// Bits 204:255 (bytes 24:32, bits 192:256, shift 12, mask 51). | ||||
| 	// Note: not bytes 25:33, shift 4, to avoid overread. | ||||
| 	v.l4 = binary.LittleEndian.Uint64(x[24:32]) >> 12 | ||||
| 	v.l4 &= maskLow51Bits | ||||
|  | ||||
| 	return v, nil | ||||
| } | ||||
|  | ||||
| // Bytes returns the canonical 32-byte little-endian encoding of v. | ||||
| func (v *Element) Bytes() []byte { | ||||
| 	// This function is outlined to make the allocations inline in the caller | ||||
| 	// rather than happen on the heap. | ||||
| 	var out [32]byte | ||||
| 	return v.bytes(&out) | ||||
| } | ||||
|  | ||||
| func (v *Element) bytes(out *[32]byte) []byte { | ||||
| 	t := *v | ||||
| 	t.reduce() | ||||
|  | ||||
| 	var buf [8]byte | ||||
| 	for i, l := range [5]uint64{t.l0, t.l1, t.l2, t.l3, t.l4} { | ||||
| 		bitsOffset := i * 51 | ||||
| 		binary.LittleEndian.PutUint64(buf[:], l<<uint(bitsOffset%8)) | ||||
| 		for i, bb := range buf { | ||||
| 			off := bitsOffset/8 + i | ||||
| 			if off >= len(out) { | ||||
| 				break | ||||
| 			} | ||||
| 			out[off] |= bb | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return out[:] | ||||
| } | ||||
|  | ||||
| // Equal returns 1 if v and u are equal, and 0 otherwise. | ||||
| func (v *Element) Equal(u *Element) int { | ||||
| 	sa, sv := u.Bytes(), v.Bytes() | ||||
| 	return subtle.ConstantTimeCompare(sa, sv) | ||||
| } | ||||
|  | ||||
| // mask64Bits returns 0xffffffff if cond is 1, and 0 otherwise. | ||||
| func mask64Bits(cond int) uint64 { return ^(uint64(cond) - 1) } | ||||
|  | ||||
| // Select sets v to a if cond == 1, and to b if cond == 0. | ||||
| func (v *Element) Select(a, b *Element, cond int) *Element { | ||||
| 	m := mask64Bits(cond) | ||||
| 	v.l0 = (m & a.l0) | (^m & b.l0) | ||||
| 	v.l1 = (m & a.l1) | (^m & b.l1) | ||||
| 	v.l2 = (m & a.l2) | (^m & b.l2) | ||||
| 	v.l3 = (m & a.l3) | (^m & b.l3) | ||||
| 	v.l4 = (m & a.l4) | (^m & b.l4) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // Swap swaps v and u if cond == 1 or leaves them unchanged if cond == 0, and returns v. | ||||
| func (v *Element) Swap(u *Element, cond int) { | ||||
| 	m := mask64Bits(cond) | ||||
| 	t := m & (v.l0 ^ u.l0) | ||||
| 	v.l0 ^= t | ||||
| 	u.l0 ^= t | ||||
| 	t = m & (v.l1 ^ u.l1) | ||||
| 	v.l1 ^= t | ||||
| 	u.l1 ^= t | ||||
| 	t = m & (v.l2 ^ u.l2) | ||||
| 	v.l2 ^= t | ||||
| 	u.l2 ^= t | ||||
| 	t = m & (v.l3 ^ u.l3) | ||||
| 	v.l3 ^= t | ||||
| 	u.l3 ^= t | ||||
| 	t = m & (v.l4 ^ u.l4) | ||||
| 	v.l4 ^= t | ||||
| 	u.l4 ^= t | ||||
| } | ||||
|  | ||||
| // IsNegative returns 1 if v is negative, and 0 otherwise. | ||||
| func (v *Element) IsNegative() int { | ||||
| 	return int(v.Bytes()[0] & 1) | ||||
| } | ||||
|  | ||||
| // Absolute sets v to |u|, and returns v. | ||||
| func (v *Element) Absolute(u *Element) *Element { | ||||
| 	return v.Select(new(Element).Negate(u), u, u.IsNegative()) | ||||
| } | ||||
|  | ||||
| // Multiply sets v = x * y, and returns v. | ||||
| func (v *Element) Multiply(x, y *Element) *Element { | ||||
| 	feMul(v, x, y) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // Square sets v = x * x, and returns v. | ||||
| func (v *Element) Square(x *Element) *Element { | ||||
| 	feSquare(v, x) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // Mult32 sets v = x * y, and returns v. | ||||
| func (v *Element) Mult32(x *Element, y uint32) *Element { | ||||
| 	x0lo, x0hi := mul51(x.l0, y) | ||||
| 	x1lo, x1hi := mul51(x.l1, y) | ||||
| 	x2lo, x2hi := mul51(x.l2, y) | ||||
| 	x3lo, x3hi := mul51(x.l3, y) | ||||
| 	x4lo, x4hi := mul51(x.l4, y) | ||||
| 	v.l0 = x0lo + 19*x4hi // carried over per the reduction identity | ||||
| 	v.l1 = x1lo + x0hi | ||||
| 	v.l2 = x2lo + x1hi | ||||
| 	v.l3 = x3lo + x2hi | ||||
| 	v.l4 = x4lo + x3hi | ||||
| 	// The hi portions are going to be only 32 bits, plus any previous excess, | ||||
| 	// so we can skip the carry propagation. | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // mul51 returns lo + hi * 2⁵¹ = a * b. | ||||
| func mul51(a uint64, b uint32) (lo uint64, hi uint64) { | ||||
| 	mh, ml := bits.Mul64(a, uint64(b)) | ||||
| 	lo = ml & maskLow51Bits | ||||
| 	hi = (mh << 13) | (ml >> 51) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // Pow22523 set v = x^((p-5)/8), and returns v. (p-5)/8 is 2^252-3. | ||||
| func (v *Element) Pow22523(x *Element) *Element { | ||||
| 	var t0, t1, t2 Element | ||||
|  | ||||
| 	t0.Square(x)             // x^2 | ||||
| 	t1.Square(&t0)           // x^4 | ||||
| 	t1.Square(&t1)           // x^8 | ||||
| 	t1.Multiply(x, &t1)      // x^9 | ||||
| 	t0.Multiply(&t0, &t1)    // x^11 | ||||
| 	t0.Square(&t0)           // x^22 | ||||
| 	t0.Multiply(&t1, &t0)    // x^31 | ||||
| 	t1.Square(&t0)           // x^62 | ||||
| 	for i := 1; i < 5; i++ { // x^992 | ||||
| 		t1.Square(&t1) | ||||
| 	} | ||||
| 	t0.Multiply(&t1, &t0)     // x^1023 -> 1023 = 2^10 - 1 | ||||
| 	t1.Square(&t0)            // 2^11 - 2 | ||||
| 	for i := 1; i < 10; i++ { // 2^20 - 2^10 | ||||
| 		t1.Square(&t1) | ||||
| 	} | ||||
| 	t1.Multiply(&t1, &t0)     // 2^20 - 1 | ||||
| 	t2.Square(&t1)            // 2^21 - 2 | ||||
| 	for i := 1; i < 20; i++ { // 2^40 - 2^20 | ||||
| 		t2.Square(&t2) | ||||
| 	} | ||||
| 	t1.Multiply(&t2, &t1)     // 2^40 - 1 | ||||
| 	t1.Square(&t1)            // 2^41 - 2 | ||||
| 	for i := 1; i < 10; i++ { // 2^50 - 2^10 | ||||
| 		t1.Square(&t1) | ||||
| 	} | ||||
| 	t0.Multiply(&t1, &t0)     // 2^50 - 1 | ||||
| 	t1.Square(&t0)            // 2^51 - 2 | ||||
| 	for i := 1; i < 50; i++ { // 2^100 - 2^50 | ||||
| 		t1.Square(&t1) | ||||
| 	} | ||||
| 	t1.Multiply(&t1, &t0)      // 2^100 - 1 | ||||
| 	t2.Square(&t1)             // 2^101 - 2 | ||||
| 	for i := 1; i < 100; i++ { // 2^200 - 2^100 | ||||
| 		t2.Square(&t2) | ||||
| 	} | ||||
| 	t1.Multiply(&t2, &t1)     // 2^200 - 1 | ||||
| 	t1.Square(&t1)            // 2^201 - 2 | ||||
| 	for i := 1; i < 50; i++ { // 2^250 - 2^50 | ||||
| 		t1.Square(&t1) | ||||
| 	} | ||||
| 	t0.Multiply(&t1, &t0)     // 2^250 - 1 | ||||
| 	t0.Square(&t0)            // 2^251 - 2 | ||||
| 	t0.Square(&t0)            // 2^252 - 4 | ||||
| 	return v.Multiply(&t0, x) // 2^252 - 3 -> x^(2^252-3) | ||||
| } | ||||
|  | ||||
| // sqrtM1 is 2^((p-1)/4), which squared is equal to -1 by Euler's Criterion. | ||||
| var sqrtM1 = &Element{1718705420411056, 234908883556509, | ||||
| 	2233514472574048, 2117202627021982, 765476049583133} | ||||
|  | ||||
| // SqrtRatio sets r to the non-negative square root of the ratio of u and v. | ||||
| // | ||||
| // If u/v is square, SqrtRatio returns r and 1. If u/v is not square, SqrtRatio | ||||
| // sets r according to Section 4.3 of draft-irtf-cfrg-ristretto255-decaf448-00, | ||||
| // and returns r and 0. | ||||
| func (r *Element) SqrtRatio(u, v *Element) (R *Element, wasSquare int) { | ||||
| 	t0 := new(Element) | ||||
|  | ||||
| 	// r = (u * v3) * (u * v7)^((p-5)/8) | ||||
| 	v2 := new(Element).Square(v) | ||||
| 	uv3 := new(Element).Multiply(u, t0.Multiply(v2, v)) | ||||
| 	uv7 := new(Element).Multiply(uv3, t0.Square(v2)) | ||||
| 	rr := new(Element).Multiply(uv3, t0.Pow22523(uv7)) | ||||
|  | ||||
| 	check := new(Element).Multiply(v, t0.Square(rr)) // check = v * r^2 | ||||
|  | ||||
| 	uNeg := new(Element).Negate(u) | ||||
| 	correctSignSqrt := check.Equal(u) | ||||
| 	flippedSignSqrt := check.Equal(uNeg) | ||||
| 	flippedSignSqrtI := check.Equal(t0.Multiply(uNeg, sqrtM1)) | ||||
|  | ||||
| 	rPrime := new(Element).Multiply(rr, sqrtM1) // r_prime = SQRT_M1 * r | ||||
| 	// r = CT_SELECT(r_prime IF flipped_sign_sqrt | flipped_sign_sqrt_i ELSE r) | ||||
| 	rr.Select(rPrime, rr, flippedSignSqrt|flippedSignSqrtI) | ||||
|  | ||||
| 	r.Absolute(rr) // Choose the nonnegative square root. | ||||
| 	return r, correctSignSqrt | flippedSignSqrt | ||||
| } | ||||
							
								
								
									
										16
									
								
								vendor/filippo.io/edwards25519/field/fe_amd64.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| // Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT. | ||||
|  | ||||
| //go:build amd64 && gc && !purego | ||||
| // +build amd64,gc,!purego | ||||
|  | ||||
| package field | ||||
|  | ||||
| // feMul sets out = a * b. It works like feMulGeneric. | ||||
| // | ||||
| //go:noescape | ||||
| func feMul(out *Element, a *Element, b *Element) | ||||
|  | ||||
| // feSquare sets out = a * a. It works like feSquareGeneric. | ||||
| // | ||||
| //go:noescape | ||||
| func feSquare(out *Element, a *Element) | ||||
							
								
								
									
										379
									
								
								vendor/filippo.io/edwards25519/field/fe_amd64.s
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,379 @@ | ||||
| // Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT. | ||||
|  | ||||
| //go:build amd64 && gc && !purego | ||||
| // +build amd64,gc,!purego | ||||
|  | ||||
| #include "textflag.h" | ||||
|  | ||||
| // func feMul(out *Element, a *Element, b *Element) | ||||
| TEXT ·feMul(SB), NOSPLIT, $0-24 | ||||
| 	MOVQ a+8(FP), CX | ||||
| 	MOVQ b+16(FP), BX | ||||
|  | ||||
| 	// r0 = a0×b0 | ||||
| 	MOVQ (CX), AX | ||||
| 	MULQ (BX) | ||||
| 	MOVQ AX, DI | ||||
| 	MOVQ DX, SI | ||||
|  | ||||
| 	// r0 += 19×a1×b4 | ||||
| 	MOVQ   8(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   32(BX) | ||||
| 	ADDQ   AX, DI | ||||
| 	ADCQ   DX, SI | ||||
|  | ||||
| 	// r0 += 19×a2×b3 | ||||
| 	MOVQ   16(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   24(BX) | ||||
| 	ADDQ   AX, DI | ||||
| 	ADCQ   DX, SI | ||||
|  | ||||
| 	// r0 += 19×a3×b2 | ||||
| 	MOVQ   24(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   16(BX) | ||||
| 	ADDQ   AX, DI | ||||
| 	ADCQ   DX, SI | ||||
|  | ||||
| 	// r0 += 19×a4×b1 | ||||
| 	MOVQ   32(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   8(BX) | ||||
| 	ADDQ   AX, DI | ||||
| 	ADCQ   DX, SI | ||||
|  | ||||
| 	// r1 = a0×b1 | ||||
| 	MOVQ (CX), AX | ||||
| 	MULQ 8(BX) | ||||
| 	MOVQ AX, R9 | ||||
| 	MOVQ DX, R8 | ||||
|  | ||||
| 	// r1 += a1×b0 | ||||
| 	MOVQ 8(CX), AX | ||||
| 	MULQ (BX) | ||||
| 	ADDQ AX, R9 | ||||
| 	ADCQ DX, R8 | ||||
|  | ||||
| 	// r1 += 19×a2×b4 | ||||
| 	MOVQ   16(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   32(BX) | ||||
| 	ADDQ   AX, R9 | ||||
| 	ADCQ   DX, R8 | ||||
|  | ||||
| 	// r1 += 19×a3×b3 | ||||
| 	MOVQ   24(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   24(BX) | ||||
| 	ADDQ   AX, R9 | ||||
| 	ADCQ   DX, R8 | ||||
|  | ||||
| 	// r1 += 19×a4×b2 | ||||
| 	MOVQ   32(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   16(BX) | ||||
| 	ADDQ   AX, R9 | ||||
| 	ADCQ   DX, R8 | ||||
|  | ||||
| 	// r2 = a0×b2 | ||||
| 	MOVQ (CX), AX | ||||
| 	MULQ 16(BX) | ||||
| 	MOVQ AX, R11 | ||||
| 	MOVQ DX, R10 | ||||
|  | ||||
| 	// r2 += a1×b1 | ||||
| 	MOVQ 8(CX), AX | ||||
| 	MULQ 8(BX) | ||||
| 	ADDQ AX, R11 | ||||
| 	ADCQ DX, R10 | ||||
|  | ||||
| 	// r2 += a2×b0 | ||||
| 	MOVQ 16(CX), AX | ||||
| 	MULQ (BX) | ||||
| 	ADDQ AX, R11 | ||||
| 	ADCQ DX, R10 | ||||
|  | ||||
| 	// r2 += 19×a3×b4 | ||||
| 	MOVQ   24(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   32(BX) | ||||
| 	ADDQ   AX, R11 | ||||
| 	ADCQ   DX, R10 | ||||
|  | ||||
| 	// r2 += 19×a4×b3 | ||||
| 	MOVQ   32(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   24(BX) | ||||
| 	ADDQ   AX, R11 | ||||
| 	ADCQ   DX, R10 | ||||
|  | ||||
| 	// r3 = a0×b3 | ||||
| 	MOVQ (CX), AX | ||||
| 	MULQ 24(BX) | ||||
| 	MOVQ AX, R13 | ||||
| 	MOVQ DX, R12 | ||||
|  | ||||
| 	// r3 += a1×b2 | ||||
| 	MOVQ 8(CX), AX | ||||
| 	MULQ 16(BX) | ||||
| 	ADDQ AX, R13 | ||||
| 	ADCQ DX, R12 | ||||
|  | ||||
| 	// r3 += a2×b1 | ||||
| 	MOVQ 16(CX), AX | ||||
| 	MULQ 8(BX) | ||||
| 	ADDQ AX, R13 | ||||
| 	ADCQ DX, R12 | ||||
|  | ||||
| 	// r3 += a3×b0 | ||||
| 	MOVQ 24(CX), AX | ||||
| 	MULQ (BX) | ||||
| 	ADDQ AX, R13 | ||||
| 	ADCQ DX, R12 | ||||
|  | ||||
| 	// r3 += 19×a4×b4 | ||||
| 	MOVQ   32(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   32(BX) | ||||
| 	ADDQ   AX, R13 | ||||
| 	ADCQ   DX, R12 | ||||
|  | ||||
| 	// r4 = a0×b4 | ||||
| 	MOVQ (CX), AX | ||||
| 	MULQ 32(BX) | ||||
| 	MOVQ AX, R15 | ||||
| 	MOVQ DX, R14 | ||||
|  | ||||
| 	// r4 += a1×b3 | ||||
| 	MOVQ 8(CX), AX | ||||
| 	MULQ 24(BX) | ||||
| 	ADDQ AX, R15 | ||||
| 	ADCQ DX, R14 | ||||
|  | ||||
| 	// r4 += a2×b2 | ||||
| 	MOVQ 16(CX), AX | ||||
| 	MULQ 16(BX) | ||||
| 	ADDQ AX, R15 | ||||
| 	ADCQ DX, R14 | ||||
|  | ||||
| 	// r4 += a3×b1 | ||||
| 	MOVQ 24(CX), AX | ||||
| 	MULQ 8(BX) | ||||
| 	ADDQ AX, R15 | ||||
| 	ADCQ DX, R14 | ||||
|  | ||||
| 	// r4 += a4×b0 | ||||
| 	MOVQ 32(CX), AX | ||||
| 	MULQ (BX) | ||||
| 	ADDQ AX, R15 | ||||
| 	ADCQ DX, R14 | ||||
|  | ||||
| 	// First reduction chain | ||||
| 	MOVQ   $0x0007ffffffffffff, AX | ||||
| 	SHLQ   $0x0d, DI, SI | ||||
| 	SHLQ   $0x0d, R9, R8 | ||||
| 	SHLQ   $0x0d, R11, R10 | ||||
| 	SHLQ   $0x0d, R13, R12 | ||||
| 	SHLQ   $0x0d, R15, R14 | ||||
| 	ANDQ   AX, DI | ||||
| 	IMUL3Q $0x13, R14, R14 | ||||
| 	ADDQ   R14, DI | ||||
| 	ANDQ   AX, R9 | ||||
| 	ADDQ   SI, R9 | ||||
| 	ANDQ   AX, R11 | ||||
| 	ADDQ   R8, R11 | ||||
| 	ANDQ   AX, R13 | ||||
| 	ADDQ   R10, R13 | ||||
| 	ANDQ   AX, R15 | ||||
| 	ADDQ   R12, R15 | ||||
|  | ||||
| 	// Second reduction chain (carryPropagate) | ||||
| 	MOVQ   DI, SI | ||||
| 	SHRQ   $0x33, SI | ||||
| 	MOVQ   R9, R8 | ||||
| 	SHRQ   $0x33, R8 | ||||
| 	MOVQ   R11, R10 | ||||
| 	SHRQ   $0x33, R10 | ||||
| 	MOVQ   R13, R12 | ||||
| 	SHRQ   $0x33, R12 | ||||
| 	MOVQ   R15, R14 | ||||
| 	SHRQ   $0x33, R14 | ||||
| 	ANDQ   AX, DI | ||||
| 	IMUL3Q $0x13, R14, R14 | ||||
| 	ADDQ   R14, DI | ||||
| 	ANDQ   AX, R9 | ||||
| 	ADDQ   SI, R9 | ||||
| 	ANDQ   AX, R11 | ||||
| 	ADDQ   R8, R11 | ||||
| 	ANDQ   AX, R13 | ||||
| 	ADDQ   R10, R13 | ||||
| 	ANDQ   AX, R15 | ||||
| 	ADDQ   R12, R15 | ||||
|  | ||||
| 	// Store output | ||||
| 	MOVQ out+0(FP), AX | ||||
| 	MOVQ DI, (AX) | ||||
| 	MOVQ R9, 8(AX) | ||||
| 	MOVQ R11, 16(AX) | ||||
| 	MOVQ R13, 24(AX) | ||||
| 	MOVQ R15, 32(AX) | ||||
| 	RET | ||||
|  | ||||
| // func feSquare(out *Element, a *Element) | ||||
| TEXT ·feSquare(SB), NOSPLIT, $0-16 | ||||
| 	MOVQ a+8(FP), CX | ||||
|  | ||||
| 	// r0 = l0×l0 | ||||
| 	MOVQ (CX), AX | ||||
| 	MULQ (CX) | ||||
| 	MOVQ AX, SI | ||||
| 	MOVQ DX, BX | ||||
|  | ||||
| 	// r0 += 38×l1×l4 | ||||
| 	MOVQ   8(CX), AX | ||||
| 	IMUL3Q $0x26, AX, AX | ||||
| 	MULQ   32(CX) | ||||
| 	ADDQ   AX, SI | ||||
| 	ADCQ   DX, BX | ||||
|  | ||||
| 	// r0 += 38×l2×l3 | ||||
| 	MOVQ   16(CX), AX | ||||
| 	IMUL3Q $0x26, AX, AX | ||||
| 	MULQ   24(CX) | ||||
| 	ADDQ   AX, SI | ||||
| 	ADCQ   DX, BX | ||||
|  | ||||
| 	// r1 = 2×l0×l1 | ||||
| 	MOVQ (CX), AX | ||||
| 	SHLQ $0x01, AX | ||||
| 	MULQ 8(CX) | ||||
| 	MOVQ AX, R8 | ||||
| 	MOVQ DX, DI | ||||
|  | ||||
| 	// r1 += 38×l2×l4 | ||||
| 	MOVQ   16(CX), AX | ||||
| 	IMUL3Q $0x26, AX, AX | ||||
| 	MULQ   32(CX) | ||||
| 	ADDQ   AX, R8 | ||||
| 	ADCQ   DX, DI | ||||
|  | ||||
| 	// r1 += 19×l3×l3 | ||||
| 	MOVQ   24(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   24(CX) | ||||
| 	ADDQ   AX, R8 | ||||
| 	ADCQ   DX, DI | ||||
|  | ||||
| 	// r2 = 2×l0×l2 | ||||
| 	MOVQ (CX), AX | ||||
| 	SHLQ $0x01, AX | ||||
| 	MULQ 16(CX) | ||||
| 	MOVQ AX, R10 | ||||
| 	MOVQ DX, R9 | ||||
|  | ||||
| 	// r2 += l1×l1 | ||||
| 	MOVQ 8(CX), AX | ||||
| 	MULQ 8(CX) | ||||
| 	ADDQ AX, R10 | ||||
| 	ADCQ DX, R9 | ||||
|  | ||||
| 	// r2 += 38×l3×l4 | ||||
| 	MOVQ   24(CX), AX | ||||
| 	IMUL3Q $0x26, AX, AX | ||||
| 	MULQ   32(CX) | ||||
| 	ADDQ   AX, R10 | ||||
| 	ADCQ   DX, R9 | ||||
|  | ||||
| 	// r3 = 2×l0×l3 | ||||
| 	MOVQ (CX), AX | ||||
| 	SHLQ $0x01, AX | ||||
| 	MULQ 24(CX) | ||||
| 	MOVQ AX, R12 | ||||
| 	MOVQ DX, R11 | ||||
|  | ||||
| 	// r3 += 2×l1×l2 | ||||
| 	MOVQ   8(CX), AX | ||||
| 	IMUL3Q $0x02, AX, AX | ||||
| 	MULQ   16(CX) | ||||
| 	ADDQ   AX, R12 | ||||
| 	ADCQ   DX, R11 | ||||
|  | ||||
| 	// r3 += 19×l4×l4 | ||||
| 	MOVQ   32(CX), AX | ||||
| 	IMUL3Q $0x13, AX, AX | ||||
| 	MULQ   32(CX) | ||||
| 	ADDQ   AX, R12 | ||||
| 	ADCQ   DX, R11 | ||||
|  | ||||
| 	// r4 = 2×l0×l4 | ||||
| 	MOVQ (CX), AX | ||||
| 	SHLQ $0x01, AX | ||||
| 	MULQ 32(CX) | ||||
| 	MOVQ AX, R14 | ||||
| 	MOVQ DX, R13 | ||||
|  | ||||
| 	// r4 += 2×l1×l3 | ||||
| 	MOVQ   8(CX), AX | ||||
| 	IMUL3Q $0x02, AX, AX | ||||
| 	MULQ   24(CX) | ||||
| 	ADDQ   AX, R14 | ||||
| 	ADCQ   DX, R13 | ||||
|  | ||||
| 	// r4 += l2×l2 | ||||
| 	MOVQ 16(CX), AX | ||||
| 	MULQ 16(CX) | ||||
| 	ADDQ AX, R14 | ||||
| 	ADCQ DX, R13 | ||||
|  | ||||
| 	// First reduction chain | ||||
| 	MOVQ   $0x0007ffffffffffff, AX | ||||
| 	SHLQ   $0x0d, SI, BX | ||||
| 	SHLQ   $0x0d, R8, DI | ||||
| 	SHLQ   $0x0d, R10, R9 | ||||
| 	SHLQ   $0x0d, R12, R11 | ||||
| 	SHLQ   $0x0d, R14, R13 | ||||
| 	ANDQ   AX, SI | ||||
| 	IMUL3Q $0x13, R13, R13 | ||||
| 	ADDQ   R13, SI | ||||
| 	ANDQ   AX, R8 | ||||
| 	ADDQ   BX, R8 | ||||
| 	ANDQ   AX, R10 | ||||
| 	ADDQ   DI, R10 | ||||
| 	ANDQ   AX, R12 | ||||
| 	ADDQ   R9, R12 | ||||
| 	ANDQ   AX, R14 | ||||
| 	ADDQ   R11, R14 | ||||
|  | ||||
| 	// Second reduction chain (carryPropagate) | ||||
| 	MOVQ   SI, BX | ||||
| 	SHRQ   $0x33, BX | ||||
| 	MOVQ   R8, DI | ||||
| 	SHRQ   $0x33, DI | ||||
| 	MOVQ   R10, R9 | ||||
| 	SHRQ   $0x33, R9 | ||||
| 	MOVQ   R12, R11 | ||||
| 	SHRQ   $0x33, R11 | ||||
| 	MOVQ   R14, R13 | ||||
| 	SHRQ   $0x33, R13 | ||||
| 	ANDQ   AX, SI | ||||
| 	IMUL3Q $0x13, R13, R13 | ||||
| 	ADDQ   R13, SI | ||||
| 	ANDQ   AX, R8 | ||||
| 	ADDQ   BX, R8 | ||||
| 	ANDQ   AX, R10 | ||||
| 	ADDQ   DI, R10 | ||||
| 	ANDQ   AX, R12 | ||||
| 	ADDQ   R9, R12 | ||||
| 	ANDQ   AX, R14 | ||||
| 	ADDQ   R11, R14 | ||||
|  | ||||
| 	// Store output | ||||
| 	MOVQ out+0(FP), AX | ||||
| 	MOVQ SI, (AX) | ||||
| 	MOVQ R8, 8(AX) | ||||
| 	MOVQ R10, 16(AX) | ||||
| 	MOVQ R12, 24(AX) | ||||
| 	MOVQ R14, 32(AX) | ||||
| 	RET | ||||
							
								
								
									
										12
									
								
								vendor/filippo.io/edwards25519/field/fe_amd64_noasm.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| // Copyright (c) 2019 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| //go:build !amd64 || !gc || purego | ||||
| // +build !amd64 !gc purego | ||||
|  | ||||
| package field | ||||
|  | ||||
| func feMul(v, x, y *Element) { feMulGeneric(v, x, y) } | ||||
|  | ||||
| func feSquare(v, x *Element) { feSquareGeneric(v, x) } | ||||
							
								
								
									
										16
									
								
								vendor/filippo.io/edwards25519/field/fe_arm64.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| // Copyright (c) 2020 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| //go:build arm64 && gc && !purego | ||||
| // +build arm64,gc,!purego | ||||
|  | ||||
| package field | ||||
|  | ||||
| //go:noescape | ||||
| func carryPropagate(v *Element) | ||||
|  | ||||
| func (v *Element) carryPropagate() *Element { | ||||
| 	carryPropagate(v) | ||||
| 	return v | ||||
| } | ||||
							
								
								
									
										42
									
								
								vendor/filippo.io/edwards25519/field/fe_arm64.s
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| // Copyright (c) 2020 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| //go:build arm64 && gc && !purego | ||||
|  | ||||
| #include "textflag.h" | ||||
|  | ||||
| // carryPropagate works exactly like carryPropagateGeneric and uses the | ||||
| // same AND, ADD, and LSR+MADD instructions emitted by the compiler, but | ||||
| // avoids loading R0-R4 twice and uses LDP and STP. | ||||
| // | ||||
| // See https://golang.org/issues/43145 for the main compiler issue. | ||||
| // | ||||
| // func carryPropagate(v *Element) | ||||
| TEXT ·carryPropagate(SB),NOFRAME|NOSPLIT,$0-8 | ||||
| 	MOVD v+0(FP), R20 | ||||
|  | ||||
| 	LDP 0(R20), (R0, R1) | ||||
| 	LDP 16(R20), (R2, R3) | ||||
| 	MOVD 32(R20), R4 | ||||
|  | ||||
| 	AND $0x7ffffffffffff, R0, R10 | ||||
| 	AND $0x7ffffffffffff, R1, R11 | ||||
| 	AND $0x7ffffffffffff, R2, R12 | ||||
| 	AND $0x7ffffffffffff, R3, R13 | ||||
| 	AND $0x7ffffffffffff, R4, R14 | ||||
|  | ||||
| 	ADD R0>>51, R11, R11 | ||||
| 	ADD R1>>51, R12, R12 | ||||
| 	ADD R2>>51, R13, R13 | ||||
| 	ADD R3>>51, R14, R14 | ||||
| 	// R4>>51 * 19 + R10 -> R10 | ||||
| 	LSR $51, R4, R21 | ||||
| 	MOVD $19, R22 | ||||
| 	MADD R22, R10, R21, R10 | ||||
|  | ||||
| 	STP (R10, R11), 0(R20) | ||||
| 	STP (R12, R13), 16(R20) | ||||
| 	MOVD R14, 32(R20) | ||||
|  | ||||
| 	RET | ||||
							
								
								
									
										12
									
								
								vendor/filippo.io/edwards25519/field/fe_arm64_noasm.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| // Copyright (c) 2021 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| //go:build !arm64 || !gc || purego | ||||
| // +build !arm64 !gc purego | ||||
|  | ||||
| package field | ||||
|  | ||||
| func (v *Element) carryPropagate() *Element { | ||||
| 	return v.carryPropagateGeneric() | ||||
| } | ||||
							
								
								
									
										50
									
								
								vendor/filippo.io/edwards25519/field/fe_extra.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,50 @@ | ||||
| // Copyright (c) 2021 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package field | ||||
|  | ||||
| import "errors" | ||||
|  | ||||
| // This file contains additional functionality that is not included in the | ||||
| // upstream crypto/ed25519/edwards25519/field package. | ||||
|  | ||||
| // SetWideBytes sets v to x, where x is a 64-byte little-endian encoding, which | ||||
| // is reduced modulo the field order. If x is not of the right length, | ||||
| // SetWideBytes returns nil and an error, and the receiver is unchanged. | ||||
| // | ||||
| // SetWideBytes is not necessary to select a uniformly distributed value, and is | ||||
| // only provided for compatibility: SetBytes can be used instead as the chance | ||||
| // of bias is less than 2⁻²⁵⁰. | ||||
| func (v *Element) SetWideBytes(x []byte) (*Element, error) { | ||||
| 	if len(x) != 64 { | ||||
| 		return nil, errors.New("edwards25519: invalid SetWideBytes input size") | ||||
| 	} | ||||
|  | ||||
| 	// Split the 64 bytes into two elements, and extract the most significant | ||||
| 	// bit of each, which is ignored by SetBytes. | ||||
| 	lo, _ := new(Element).SetBytes(x[:32]) | ||||
| 	loMSB := uint64(x[31] >> 7) | ||||
| 	hi, _ := new(Element).SetBytes(x[32:]) | ||||
| 	hiMSB := uint64(x[63] >> 7) | ||||
|  | ||||
| 	// The output we want is | ||||
| 	// | ||||
| 	//   v = lo + loMSB * 2²⁵⁵ + hi * 2²⁵⁶ + hiMSB * 2⁵¹¹ | ||||
| 	// | ||||
| 	// which applying the reduction identity comes out to | ||||
| 	// | ||||
| 	//   v = lo + loMSB * 19 + hi * 2 * 19 + hiMSB * 2 * 19² | ||||
| 	// | ||||
| 	// l0 will be the sum of a 52 bits value (lo.l0), plus a 5 bits value | ||||
| 	// (loMSB * 19), a 6 bits value (hi.l0 * 2 * 19), and a 10 bits value | ||||
| 	// (hiMSB * 2 * 19²), so it fits in a uint64. | ||||
|  | ||||
| 	v.l0 = lo.l0 + loMSB*19 + hi.l0*2*19 + hiMSB*2*19*19 | ||||
| 	v.l1 = lo.l1 + hi.l1*2*19 | ||||
| 	v.l2 = lo.l2 + hi.l2*2*19 | ||||
| 	v.l3 = lo.l3 + hi.l3*2*19 | ||||
| 	v.l4 = lo.l4 + hi.l4*2*19 | ||||
|  | ||||
| 	return v.carryPropagate(), nil | ||||
| } | ||||