Compare commits
	
		
			448 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 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 | ||
|   | afcd362cd1 | ||
|   | 0452be0cb3 | ||
|   | 1624f10773 | ||
|   | 8764be7461 | ||
|   | 5dd15ef8e7 | ||
|   | 4ac6366706 | ||
|   | adc0912efa | ||
|   | 536823ce55 | ||
|   | 207cd24edb | ||
|   | b039da1eba | ||
|   | 8fcd0f3b6f | ||
|   | 16fde6935c | ||
|   | 9592cff9fa | ||
|   | 109148988c | ||
|   | cf13fff7d2 | ||
|   | a9d8ac8bc0 | ||
|   | 1a4717b366 | ||
|   | 6cadf12260 | ||
|   | 19d47784bd | ||
|   | b89102c5fc | ||
|   | 4f20ebead3 | ||
|   | a9f89dbc64 | ||
|   | 58ea1e07d2 | ||
|   | 6de4c7e971 | ||
|   | 03dc51ffa2 | ||
|   | aef2dcdfdd | ||
|   | 0494119bf4 | ||
|   | 0a17e21119 | ||
|   | 52e2f926f4 | ||
|   | 611fb279bc | ||
|   | 41b4e64be9 | ||
|   | 0d7315249d | ||
|   | 4913766d58 | ||
|   | 92da8c7044 | ||
|   | 9dba3d5385 | ||
|   | 2d3c26a4b2 | ||
|   | 8eba2d3e50 | ||
|   | a8d4a27de1 | ||
|   | c42167c6f4 | ||
|   | 44d182e2f9 | ||
|   | ad95e35687 | ||
|   | 640a9995f4 | ||
|   | 95625f6871 | ||
|   | 2c20f72a9c | ||
|   | 5ad788e768 | ||
|   | ed98c586c6 | ||
|   | 3e865708d6 | ||
|   | c3bcbd63c0 | ||
|   | 29e29439ee | ||
|   | 0c19716f44 | ||
|   | b24e1bafa1 | ||
|   | 64b899ac89 | ||
|   | aa274e5ab7 | ||
|   | 7b3eaf3ccf | ||
|   | 1a5353d768 | ||
|   | eff41759bc | ||
|   | c23252ab53 | ||
|   | 1a3c57a031 | ||
|   | 4cc2c914e6 | ||
|   | cbb46293ab | ||
|   | 765e00c949 | ||
|   | 662359908b | ||
|   | 0d99766686 | ||
|   | ae3bc3358b | ||
|   | 1e0b4532bd | ||
|   | 4f8b19c686 | ||
|   | 84ab223b81 | ||
|   | 2bb21262d4 | ||
|   | 3188a9ffe6 | ||
|   | 61569a8610 | ||
|   | 075a84427f | ||
|   | 950f2759bd | ||
|   | 25c82ddf02 | ||
|   | 2d98df6122 | ||
|   | 219a5453f9 | ||
|   | 214a6a1386 | ||
|   | e7781dc79c | ||
|   | 10c4bd1ac8 | ||
|   | a42e488e58 | ||
|   | 06eb89b05b | ||
|   | 91c58ec027 | ||
|   | 8b26e42a3a | ||
|   | acca011f15 | ||
|   | 2f59abdda7 | ||
|   | 17747a5c88 | ||
|   | cec63546ff | ||
|   | 75f67d2de4 | ||
|   | 712385ffd5 | ||
|   | ad90cf09fe | ||
|   | f9928c9e25 | ||
|   | a0741d99b8 | ||
|   | c63f08c811 | ||
|   | 58b6c4d277 | ||
|   | 27c02549c8 | ||
|   | 88d371c71c | ||
|   | b339524613 | ||
|   | d5feda5c8a | ||
|   | 2f506425c2 | ||
|   | e8167ee3d7 | ||
|   | 2f5e211065 | ||
|   | 39f4fb3446 | ||
|   | 56159b9bce | ||
|   | b2af76e7dc | ||
|   | 491fe35397 | ||
|   | b451285af7 | ||
|   | 63a1847cdc | ||
|   | 4e50fd8649 | ||
|   | dfdffa0027 | ||
|   | ebd2073144 | ||
|   | 1e94b716fb | ||
|   | 2a41abb3d1 | ||
|   | 2d2bebe976 | ||
|   | e1629994bd | ||
|   | e3d8fe4fd8 | ||
|   | 23d8742f0d | ||
|   | 3b6a8be07b | ||
|   | 71a5b72aff | ||
|   | 213bf349c3 | ||
|   | a94fe55886 | ||
|   | 9b22f16497 | ||
|   | 2977a5957e | ||
|   | f70d1c897a | ||
|   | a4a3525265 | ||
|   | a6dd8446e4 | ||
|   | 7bf9e1cfb3 | ||
|   | f291832a77 | ||
|   | 1fee323247 | ||
|   | a41accd033 | ||
|   | 37f7caf7f3 | ||
|   | 5847f7758c | ||
|   | bce736993e | ||
|   | 5636992446 | ||
|   | f996a2b7ae | ||
|   | 587de96ab3 | ||
|   | 80eb1cd202 | ||
|   | bbf594c815 | ||
|   | 2f0f2ee40d | ||
|   | 96022d3aaf | ||
|   | 8eb5e3cbf8 | ||
|   | ddc2625934 | ||
|   | 7f7ca697a0 | ||
|   | 900375679b | ||
|   | 9440b9e313 | ||
|   | 393f9e998b | ||
|   | ba0bfe70a8 | ||
|   | 3c4a3e3f75 | ||
|   | 274fb09ed4 | ||
|   | d44598a900 | ||
|   | c9cfa59f54 | ||
|   | 7062234331 | ||
|   | 9754569525 | ||
|   | 52a071e34d | ||
|   | 2d8f749e36 | ||
|   | a18cb74f03 | ||
|   | 6c442e239d | ||
|   | eaf92fca4d | ||
|   | 06b7bad714 | ||
|   | 19eec2ed03 | ||
|   | d99c54343a | ||
|   | 308a110000 | ||
|   | 4f406b2ce6 | ||
|   | e564c555d7 | ||
|   | f7ec9af9e8 | ||
|   | 4d93a774ce | ||
|   | 2595dd30bf | ||
|   | 9190365289 | ||
|   | 57794b3b9f | ||
|   | 3c36f651be | ||
|   | 8e6ddadba2 | ||
|   | 8a87a71927 | ||
|   | 0047e6f523 | ||
|   | 7183095a28 | ||
|   | 13c90893c7 | ||
|   | 976fbcd07f | 
							
								
								
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| Dockerfile | ||||
| tgs.Dockerfile | ||||
							
								
								
									
										71
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,71 @@ | ||||
| # For most projects, this workflow file will not need changing; you simply need | ||||
| # to commit it to your repository. | ||||
| # | ||||
| # You may wish to alter this file to override the set of languages analyzed, | ||||
| # or to provide custom queries or build logic. | ||||
| name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [master] | ||||
|   pull_request: | ||||
|     # The branches below must be a subset of the branches above | ||||
|     branches: [master] | ||||
|   schedule: | ||||
|     - cron: '0 16 * * 1' | ||||
|  | ||||
| jobs: | ||||
|   analyze: | ||||
|     name: Analyze | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         # Override automatic language detection by changing the below list | ||||
|         # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] | ||||
|         language: ['go'] | ||||
|         # Learn more... | ||||
|         # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection | ||||
|  | ||||
|     steps: | ||||
|     - name: Checkout repository | ||||
|       uses: actions/checkout@v2 | ||||
|       with: | ||||
|         # We must fetch at least the immediate parents so that if this is | ||||
|         # a pull request then we can checkout the head. | ||||
|         fetch-depth: 2 | ||||
|  | ||||
|     # If this run was triggered by a pull request event, then checkout | ||||
|     # the head of the pull request instead of the merge commit. | ||||
|     - run: git checkout HEAD^2 | ||||
|       if: ${{ github.event_name == 'pull_request' }} | ||||
|  | ||||
|     # Initializes the CodeQL tools for scanning. | ||||
|     - name: Initialize CodeQL | ||||
|       uses: github/codeql-action/init@v1 | ||||
|       with: | ||||
|         languages: ${{ matrix.language }} | ||||
|         # If you wish to specify custom queries, you can do so here or in a config file. | ||||
|         # By default, queries listed here will override any specified in a config file.  | ||||
|         # Prefix the list here with "+" to use these queries and those in the config file. | ||||
|         # queries: ./path/to/local/query, your-org/your-repo/queries@main | ||||
|  | ||||
|     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). | ||||
|     # If this step fails, then you should remove it and run the build manually (see below) | ||||
|     - name: Autobuild | ||||
|       uses: github/codeql-action/autobuild@v1 | ||||
|  | ||||
|     # ℹ️ Command-line programs to run using the OS shell. | ||||
|     # 📚 https://git.io/JvXDl | ||||
|  | ||||
|     # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines | ||||
|     #    and modify them (or add more) to build your code if your project | ||||
|     #    uses a compiled language | ||||
|  | ||||
|     #- run: | | ||||
|     #   make bootstrap | ||||
|     #   make release | ||||
|  | ||||
|     - name: Perform CodeQL Analysis | ||||
|       uses: github/codeql-action/analyze@v1 | ||||
							
								
								
									
										58
									
								
								.github/workflows/development.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | ||||
| name: Development | ||||
| on: [push, pull_request] | ||||
| jobs: | ||||
|   lint: | ||||
|     name: golangci-lint | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|         with: | ||||
|           fetch-depth: 20 | ||||
|       - name: Run golangci-lint | ||||
|         uses: golangci/golangci-lint-action@v3 | ||||
|         with: | ||||
|           version: latest | ||||
|           args: "-v --new-from-rev HEAD~5 --timeout=5m" | ||||
|   test-build-upload: | ||||
|     strategy: | ||||
|       matrix: | ||||
|         go-version: [1.22.x] | ||||
|         platform: [ubuntu-latest] | ||||
|     runs-on: ${{ matrix.platform }} | ||||
|     steps: | ||||
|     - name: Install Go | ||||
|       uses: actions/setup-go@v3 | ||||
|       with: | ||||
|         go-version: ${{ matrix.go-version }} | ||||
|         stable: false | ||||
|     - name: Checkout code | ||||
|       uses: actions/checkout@v3 | ||||
|       with: | ||||
|           fetch-depth: 0 | ||||
|     - name: Test | ||||
|       run: go test ./... -mod=vendor | ||||
|     - name: Build | ||||
|       run: | | ||||
|         mkdir -p output/{win,lin,arm,mac} | ||||
|         VERSION=$(git describe --tags) | ||||
|         CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64 | ||||
|         CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe | ||||
|         CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64 | ||||
|     - name: Upload linux 64-bit | ||||
|       if: startsWith(matrix.go-version,'1.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.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.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 | ||||
| @@ -91,7 +91,6 @@ linters-settings: | ||||
|     # Correct spellings using locale preferences for US or UK. | ||||
|     # Default is to use a neutral variety of English. | ||||
|     # Setting locale to US will correct the British spelling of 'colour' to 'color'. | ||||
|     locale: US | ||||
|   lll: | ||||
|     # max line length, lines longer will be reported. Default is 120. | ||||
|     # '\t' is counted as 1 character by default, and can be changed with the tab-width option | ||||
| @@ -176,8 +175,46 @@ linters: | ||||
|     - prealloc | ||||
|     - wsl | ||||
|     - gomnd | ||||
|  | ||||
|  | ||||
|     - godox | ||||
|     - goerr113 | ||||
|     - testpackage | ||||
|     - godot | ||||
|     - interfacer | ||||
|     - goheader | ||||
|     - noctx | ||||
|     - gci | ||||
|     - errorlint | ||||
|     - nlreturn | ||||
|     - exhaustivestruct | ||||
|     - forbidigo | ||||
|     - wrapcheck | ||||
|     - varnamelen | ||||
|     - ireturn | ||||
|     - errorlint | ||||
|     - tparallel | ||||
|     - wrapcheck | ||||
|     - paralleltest | ||||
|     - makezero | ||||
|     - thelper | ||||
|     - cyclop | ||||
|     - revive | ||||
|     - importas | ||||
|     - gomoddirectives | ||||
|     - promlinter | ||||
|     - tagliatelle | ||||
|     - errname | ||||
|     - typecheck | ||||
|     - 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: | ||||
|   - | ||||
|   | ||||
							
								
								
									
										56
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						| @@ -1,56 +0,0 @@ | ||||
| language: go | ||||
| go_import_path: github.com/42wim/matterbridge | ||||
|  | ||||
| # We have everything vendored so this helps TravisCI not run `go get ...`. | ||||
| install: true | ||||
|  | ||||
| git: | ||||
|   depth: 200 | ||||
|  | ||||
| notifications: | ||||
|   email: false | ||||
|  | ||||
| branches: | ||||
|   only: | ||||
|   - master | ||||
|   - /.*/ | ||||
|  | ||||
| jobs: | ||||
|   include: | ||||
|   - stage: lint | ||||
|     # Run linting in one Go environment only. | ||||
|     script: ./ci/lint.sh | ||||
|     go: 1.14.x | ||||
|     env: | ||||
|     - GO111MODULE=on | ||||
|     - GOLANGCI_VERSION="v1.23.7" | ||||
|   - stage: test | ||||
|     # Run tests in a combination of Go environments. | ||||
|     script: ./ci/test.sh | ||||
|     go: 1.13.x | ||||
|     env: | ||||
|     - GOFLAGS=-mod=vendor | ||||
|   - script: ./ci/test.sh | ||||
|     go: 1.13.x | ||||
|     env: | ||||
|     - GO111MODULE=on | ||||
|   - script: ./ci/test.sh | ||||
|     go: 1.14.x | ||||
|     env: | ||||
|     - GO111MODULE=on | ||||
|     - REPORT_COVERAGE=1 | ||||
|     - BINDEPLOY=1 | ||||
|  | ||||
| before_deploy: /bin/bash ci/bintray.sh | ||||
|  | ||||
| deploy: | ||||
|   on: | ||||
|     all_branches: true | ||||
|     condition: $BINDEPLOY = 1 | ||||
|   provider: bintray | ||||
|   edge: | ||||
|     branch: v1.8.47 | ||||
|   file: ci/deploy.json | ||||
|   user: 42wim | ||||
|   key: | ||||
|     secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI=" | ||||
							
								
								
									
										19
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						| @@ -1,13 +1,14 @@ | ||||
| FROM alpine:edge AS builder | ||||
| FROM alpine AS builder | ||||
|  | ||||
| COPY . /go/src/github.com/42wim/matterbridge | ||||
| RUN apk update && 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 | ||||
| COPY . /go/src/matterbridge | ||||
| RUN apk --no-cache add go git \ | ||||
|         && 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:edge | ||||
| FROM alpine | ||||
| RUN apk --no-cache add ca-certificates mailcap | ||||
| COPY --from=builder /bin/matterbridge /bin/matterbridge | ||||
| ENTRYPOINT ["/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"] | ||||
|   | ||||
							
								
								
									
										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"] | ||||
							
								
								
									
										259
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -9,27 +9,26 @@ Letting people be where they want to be.<br /> | ||||
|  | ||||
|    <sup> | ||||
|  | ||||
| [Discord][mb-discord] | | ||||
| [Gitter][mb-gitter] | | ||||
| [IRC][mb-irc] | | ||||
| [Discord][mb-discord] | | ||||
| [Keybase][mb-keybase] | | ||||
| [Matrix][mb-matrix] | | ||||
| [Slack][mb-slack] | | ||||
| [Mattermost][mb-mattermost] | | ||||
| [MSTeams][mb-msteams] | | ||||
| [Rocket.Chat][mb-rocketchat] | | ||||
| [XMPP][mb-xmpp] | | ||||
| [Slack][mb-slack] | | ||||
| [Telegram][mb-telegram] | | ||||
| [Twitch][mb-twitch] | | ||||
| [WhatsApp][mb-whatsapp] | | ||||
| [XMPP][mb-xmpp] | | ||||
| [Zulip][mb-zulip] | | ||||
| [Telegram][mb-telegram] | | ||||
| [Keybase][mb-keybase] | | ||||
| [MSTeams][mb-msteams] | | ||||
| And more... | ||||
| </sup> | ||||
|  | ||||
| --- | ||||
|  | ||||
| [](https://github.com/42wim/matterbridge/releases/latest) | ||||
| [](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion) | ||||
| [](https://codeclimate.com/github/42wim/matterbridge/maintainability) | ||||
| [](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br /> | ||||
|  | ||||
| @@ -59,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](#articles) | ||||
|   - [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 | ||||
|  | ||||
| @@ -87,30 +88,47 @@ And more... | ||||
|  | ||||
| ### Natively supported | ||||
|  | ||||
| - [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x | ||||
| - [IRC](http://www.mirc.com/servers.html) | ||||
| - [XMPP](https://xmpp.org) | ||||
| - [Gitter](https://gitter.im) | ||||
| - [Slack](https://slack.com) | ||||
| - [Discord](https://discordapp.com) | ||||
| - [Telegram](https://telegram.org) | ||||
| - [Rocket.chat](https://rocket.chat) | ||||
| - [Matrix](https://matrix.org) | ||||
| - [Microsoft Teams](https://teams.microsoft.com) | ||||
| - [Steam](https://store.steampowered.com/) | ||||
| - [Twitch](https://twitch.tv) | ||||
| - [Ssh-chat](https://github.com/shazow/ssh-chat) | ||||
| - [WhatsApp](https://www.whatsapp.com/) | ||||
| - [Zulip](https://zulipchat.com) | ||||
| - [Gitter](https://gitter.im) | ||||
| - [Harmony](https://harmonyapp.io) | ||||
| - [IRC](http://www.mirc.com/servers.html) | ||||
| - [Keybase](https://keybase.io) | ||||
| - [Matrix](https://matrix.org) | ||||
| - [Mattermost](https://github.com/mattermost/mattermost-server/) | ||||
| - [Microsoft Teams](https://teams.microsoft.com) | ||||
| - [Mumble](https://www.mumble.info/) | ||||
| - [Nextcloud Talk](https://nextcloud.com/talk/) | ||||
| - [Rocket.chat](https://rocket.chat) | ||||
| - [Slack](https://slack.com) | ||||
| - [Ssh-chat](https://github.com/shazow/ssh-chat) | ||||
| - ~~[Steam](https://store.steampowered.com/)~~ | ||||
|   - Not supported anymore, see [here](https://github.com/Philipp15b/go-steam/issues/94) for more info. | ||||
| - [Telegram](https://telegram.org) | ||||
| - [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) | ||||
| - [Reddit](https://github.com/bonehurtingjuice/mattereddit) | ||||
| - [Facebook messenger](https://github.com/VictorNine/fbridge) | ||||
| - [Discourse](https://github.com/DeclanHoare/matterbabble) | ||||
| - [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430) | ||||
| - [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 | ||||
|  | ||||
| @@ -119,62 +137,115 @@ 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) | ||||
| - [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 | ||||
|  | ||||
| Questions or want to test on your favorite platform? Join below: | ||||
|  | ||||
| - [Discord][mb-discord] | ||||
| - [Gitter][mb-gitter] | ||||
| - [IRC][mb-irc] | ||||
| - [Discord][mb-discord] | ||||
| - [Keybase][mb-keybase] | ||||
| - [Matrix][mb-matrix] | ||||
| - [Slack][mb-slack] | ||||
| - [Mattermost][mb-mattermost] | ||||
| - [Rocket.Chat][mb-rocketchat] | ||||
| - [XMPP][mb-xmpp] (matterbridge@conference.jabber.de) | ||||
| - [Twitch][mb-twitch] | ||||
| - [Zulip][mb-zulip] | ||||
| - [Slack][mb-slack] | ||||
| - [Telegram][mb-telegram] | ||||
| - [Keybase][mb-keybase] | ||||
| - [Twitch][mb-twitch] | ||||
| - [XMPP][mb-xmpp] (matterbridge@conference.jabber.de) | ||||
| - [Zulip][mb-zulip] | ||||
|  | ||||
| ## Screenshots | ||||
|  | ||||
| See https://github.com/42wim/matterbridge/wiki | ||||
| See <https://github.com/42wim/matterbridge/wiki> | ||||
|  | ||||
| ## Installing / upgrading | ||||
|  | ||||
| ### Binaries | ||||
|  | ||||
| - Latest stable release [v1.17.2](https://github.com/42wim/matterbridge/releases/latest) | ||||
| - Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/) | ||||
| - 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) and follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. | ||||
| To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest). 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.12+ 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 install github.com/42wim/matterbridge | ||||
| ``` | ||||
| go get 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: | ||||
|  | ||||
| ```bash | ||||
| $ ls ~/go/bin/ | ||||
| matterbridge | ||||
| ``` | ||||
| @@ -199,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] | ||||
| @@ -216,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]] | ||||
| @@ -257,7 +328,7 @@ RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> " | ||||
|  | ||||
| See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. | ||||
|  | ||||
| ``` | ||||
| ```bash | ||||
| Usage of ./matterbridge: | ||||
|   -conf string | ||||
|         config file (default "matterbridge.toml") | ||||
| @@ -273,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) | ||||
| @@ -294,18 +369,26 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) | ||||
| - [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) | ||||
| - [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge) | ||||
| - [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge) | ||||
| - [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 | ||||
| ## Articles / Tutorials | ||||
|  | ||||
| - [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3) | ||||
| - https://mattermost.com/blog/connect-irc-to-mattermost/ | ||||
| - https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/ | ||||
| - https://blog.brightscout.com/top-10-mattermost-integrations/ | ||||
| - http://bencey.co.nz/2018/09/17/bridge/ | ||||
| - https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/ | ||||
| - https://kopano.com/blog/matterbridge-bridging-mattermost-chat/ | ||||
| - https://www.stitcher.com/s/?eid=52382713 | ||||
| - https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/ | ||||
| - <https://mattermost.com/blog/connect-irc-to-mattermost/> | ||||
| - <https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/> | ||||
| - <https://blog.brightscout.com/top-10-mattermost-integrations/> | ||||
| - <https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/> | ||||
| - <https://kopano.com/blog/matterbridge-bridging-mattermost-chat/> | ||||
| - <https://www.stitcher.com/s/?eid=52382713> | ||||
| - <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/> | ||||
| - <https://userlinux.net/mattermost-and-matterbridge.html> | ||||
| - <https://nextcloud.com/blog/bridging-chat-services-in-talk/> | ||||
| - <https://minecraftchest1.wordpress.com/2021/06/05/how-to-install-and-setup-matterbridge/> | ||||
| - Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc) | ||||
|  | ||||
| ## Thanks | ||||
|  | ||||
| @@ -318,38 +401,44 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) | ||||
|  | ||||
| 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 | ||||
| - irc - https://github.com/lrstanley/girc | ||||
| - mattermost - https://github.com/mattermost/mattermost-server | ||||
| - matrix - https://github.com/matrix-org/gomatrix | ||||
| - sshchat - https://github.com/shazow/ssh-chat | ||||
| - slack - https://github.com/nlopes/slack | ||||
| - steam - https://github.com/Philipp15b/go-steam | ||||
| - telegram - https://github.com/go-telegram-bot-api/telegram-bot-api | ||||
| - xmpp - https://github.com/mattn/go-xmpp | ||||
| - whatsapp - https://github.com/Rhymen/go-whatsapp/ | ||||
| - zulip - https://github.com/ifo/gozulipbot | ||||
| - tengo - https://github.com/d5/tengo | ||||
| - keybase - https://github.com/keybase/go-keybase-chat-bot | ||||
| - msgraph.go - https://github.com/yaegashi/msgraph.go | ||||
| - discord - <https://github.com/bwmarrin/discordgo> | ||||
| - echo - <https://github.com/labstack/echo> | ||||
| - 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> | ||||
| - mattermost - <https://github.com/mattermost/mattermost-server> | ||||
| - 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> | ||||
| - telegram - <https://github.com/go-telegram-bot-api/telegram-bot-api> | ||||
| - tengo - <https://github.com/d5/tengo> | ||||
| - vk - <https://github.com/SevereCloud/vksdk> | ||||
| - whatsapp - <https://github.com/Rhymen/go-whatsapp> | ||||
| - whatsapp - <https://github.com/tulir/whatsmeow> | ||||
| - xmpp - <https://github.com/mattn/go-xmpp> | ||||
| - zulip - <https://github.com/ifo/gozulipbot> | ||||
|  | ||||
| <!-- Links --> | ||||
|  | ||||
| [mb-gitter]: https://gitter.im/42wim/matterbridge | ||||
| [mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat | ||||
| [mb-discord]: https://discord.gg/AkKPtrQ | ||||
| [mb-gitter]: https://gitter.im/42wim/matterbridge | ||||
| [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-slack]: https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA | ||||
| [mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e | ||||
| [mb-msteams]: https://teams.microsoft.com/join/hj92x75gd3y7 | ||||
| [mb-rocketchat]: https://open.rocket.chat/channel/matterbridge | ||||
| [mb-xmpp]: https://inverse.chat/ | ||||
| [mb-slack]: https://join.slack.com/t/matterbridgechat/shared_invite/zt-2ourq2h2-7YvyYBq2WFGC~~zEzA68_Q | ||||
| [mb-telegram]: https://t.me/Matterbridge | ||||
| [mb-twitch]: https://www.twitch.tv/matterbridge | ||||
| [mb-whatsapp]: https://www.whatsapp.com/ | ||||
| [mb-keybase]: https://keybase.io/team/matterbridge | ||||
| [mb-xmpp]: https://inverse.chat/ | ||||
| [mb-zulip]: https://matterbridge.zulipchat.com/register/ | ||||
| [mb-telegram]: https://t.me/Matterbridge | ||||
| [mb-msteams]: https://teams.microsoft.com/join/hj92x75gd3y7 | ||||
|   | ||||
| @@ -1,22 +1,28 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"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/zfjagann/golang-ring" | ||||
| 	"github.com/mitchellh/mapstructure" | ||||
| 	ring "github.com/zfjagann/golang-ring" | ||||
| ) | ||||
|  | ||||
| type API struct { | ||||
| 	Messages ring.Ring | ||||
| 	sync.RWMutex | ||||
| 	*bridge.Config | ||||
| 	mrouter *melody.Melody | ||||
| } | ||||
|  | ||||
| type Message struct { | ||||
| @@ -32,6 +38,32 @@ func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	e := echo.New() | ||||
| 	e.HideBanner = true | ||||
| 	e.HidePort = true | ||||
|  | ||||
| 	b.mrouter = melody.New() | ||||
| 	b.mrouter.HandleMessage(func(s *melody.Session, msg []byte) { | ||||
| 		message := config.Message{} | ||||
| 		err := json.Unmarshal(msg, &message) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("failed to decode message from byte[] '%s'", string(msg)) | ||||
| 			return | ||||
| 		} | ||||
| 		b.handleWebsocketMessage(message, s) | ||||
| 	}) | ||||
| 	b.mrouter.HandleConnect(func(session *melody.Session) { | ||||
| 		greet := b.getGreeting() | ||||
| 		data, err := json.Marshal(greet) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("failed to encode message '%v'", greet) | ||||
| 			return | ||||
| 		} | ||||
| 		err = session.Write(data) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("failed to write message '%s'", string(data)) | ||||
| 			return | ||||
| 		} | ||||
| 		// TODO: send message history buffer from `b.Messages` here | ||||
| 	}) | ||||
|  | ||||
| 	b.Messages = ring.Ring{} | ||||
| 	if b.GetInt("Buffer") != 0 { | ||||
| 		b.Messages.SetCapacity(b.GetInt("Buffer")) | ||||
| @@ -41,9 +73,17 @@ func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 			return key == b.GetString("Token"), nil | ||||
| 		})) | ||||
| 	} | ||||
|  | ||||
| 	// Set RemoteNickFormat to a sane default | ||||
| 	if !b.IsKeySet("RemoteNickFormat") { | ||||
| 		b.Log.Debugln("RemoteNickFormat is unset, defaulting to \"{NICK}\"") | ||||
| 		b.Config.Config.Viper().Set(b.GetConfigKey("RemoteNickFormat"), "{NICK}") | ||||
| 	} | ||||
|  | ||||
| 	e.GET("/api/health", b.handleHealthcheck) | ||||
| 	e.GET("/api/messages", b.handleMessages) | ||||
| 	e.GET("/api/stream", b.handleStream) | ||||
| 	e.GET("/api/websocket", b.handleWebsocket) | ||||
| 	e.POST("/api/message", b.handlePostMessage) | ||||
| 	go func() { | ||||
| 		if b.GetString("BindAddress") == "" { | ||||
| @@ -58,13 +98,13 @@ func New(cfg *bridge.Config) bridge.Bridger { | ||||
| func (b *API) Connect() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *API) Disconnect() error { | ||||
| 	return nil | ||||
|  | ||||
| } | ||||
|  | ||||
| func (b *API) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	return nil | ||||
|  | ||||
| } | ||||
|  | ||||
| func (b *API) Send(msg config.Message) (string, error) { | ||||
| @@ -74,7 +114,14 @@ func (b *API) Send(msg config.Message) (string, error) { | ||||
| 	if msg.Event == config.EventMsgDelete { | ||||
| 		return "", nil | ||||
| 	} | ||||
| 	b.Messages.Enqueue(&msg) | ||||
| 	b.Log.Debugf("enqueueing message from %s on ring buffer", msg.Username) | ||||
| 	b.Messages.Enqueue(msg) | ||||
|  | ||||
| 	data, err := json.Marshal(msg) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("failed to encode message  '%s'", msg) | ||||
| 	} | ||||
| 	_ = b.mrouter.Broadcast(data) | ||||
| 	return "", nil | ||||
| } | ||||
|  | ||||
| @@ -93,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) | ||||
| @@ -106,25 +183,63 @@ func (b *API) handleMessages(c echo.Context) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *API) handleStream(c echo.Context) error { | ||||
| 	c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) | ||||
| 	c.Response().WriteHeader(http.StatusOK) | ||||
| 	greet := config.Message{ | ||||
| func (b *API) getGreeting() config.Message { | ||||
| 	return config.Message{ | ||||
| 		Event:     config.EventAPIConnected, | ||||
| 		Timestamp: time.Now(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *API) handleStream(c echo.Context) error { | ||||
| 	c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) | ||||
| 	c.Response().WriteHeader(http.StatusOK) | ||||
| 	greet := b.getGreeting() | ||||
| 	if err := json.NewEncoder(c.Response()).Encode(greet); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	c.Response().Flush() | ||||
| 	for { | ||||
| 		msg := b.Messages.Dequeue() | ||||
| 		if msg != nil { | ||||
| 			if err := json.NewEncoder(c.Response()).Encode(msg); err != nil { | ||||
| 				return err | ||||
| 		select { | ||||
| 		// TODO: this causes issues, messages should be broadcasted to all connected clients | ||||
| 		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) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *API) handleWebsocketMessage(message config.Message, s *melody.Session) { | ||||
| 	message.Channel = "api" | ||||
| 	message.Protocol = "api" | ||||
| 	message.Account = b.Account | ||||
| 	message.ID = "" | ||||
| 	message.Timestamp = time.Now() | ||||
|  | ||||
| 	data, err := json.Marshal(message) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("failed to encode message for loopback '%v'", message) | ||||
| 		return | ||||
| 	} | ||||
| 	_ = b.mrouter.BroadcastOthers(data, s) | ||||
|  | ||||
| 	b.Log.Debugf("Sending websocket message from %s on %s to gateway", message.Username, "api") | ||||
| 	b.Remote <- message | ||||
| } | ||||
|  | ||||
| func (b *API) handleWebsocket(c echo.Context) error { | ||||
| 	err := b.mrouter.HandleRequest(c.Response(), c.Request()) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("error in websocket handling  '%v'", err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"log" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| @@ -74,6 +75,7 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map | ||||
| 	for ID, channel := range channels { | ||||
| 		if !exists[ID] { | ||||
| 			b.Log.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID) | ||||
| 			time.Sleep(time.Duration(b.GetInt("JoinDelay")) * time.Millisecond) | ||||
| 			err := b.JoinChannel(channel) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| @@ -84,8 +86,16 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bridge) GetConfigKey(key string) string { | ||||
| 	return b.Account + "." + key | ||||
| } | ||||
|  | ||||
| func (b *Bridge) IsKeySet(key string) bool { | ||||
| 	return b.Config.IsKeySet(b.GetConfigKey(key)) || b.Config.IsKeySet("general."+key) | ||||
| } | ||||
|  | ||||
| func (b *Bridge) GetBool(key string) bool { | ||||
| 	val, ok := b.Config.GetBool(b.Account + "." + key) | ||||
| 	val, ok := b.Config.GetBool(b.GetConfigKey(key)) | ||||
| 	if !ok { | ||||
| 		val, _ = b.Config.GetBool("general." + key) | ||||
| 	} | ||||
| @@ -93,7 +103,7 @@ func (b *Bridge) GetBool(key string) bool { | ||||
| } | ||||
|  | ||||
| func (b *Bridge) GetInt(key string) int { | ||||
| 	val, ok := b.Config.GetInt(b.Account + "." + key) | ||||
| 	val, ok := b.Config.GetInt(b.GetConfigKey(key)) | ||||
| 	if !ok { | ||||
| 		val, _ = b.Config.GetInt("general." + key) | ||||
| 	} | ||||
| @@ -101,7 +111,7 @@ func (b *Bridge) GetInt(key string) int { | ||||
| } | ||||
|  | ||||
| func (b *Bridge) GetString(key string) string { | ||||
| 	val, ok := b.Config.GetString(b.Account + "." + key) | ||||
| 	val, ok := b.Config.GetString(b.GetConfigKey(key)) | ||||
| 	if !ok { | ||||
| 		val, _ = b.Config.GetString("general." + key) | ||||
| 	} | ||||
| @@ -109,7 +119,7 @@ func (b *Bridge) GetString(key string) string { | ||||
| } | ||||
|  | ||||
| func (b *Bridge) GetStringSlice(key string) []string { | ||||
| 	val, ok := b.Config.GetStringSlice(b.Account + "." + key) | ||||
| 	val, ok := b.Config.GetStringSlice(b.GetConfigKey(key)) | ||||
| 	if !ok { | ||||
| 		val, _ = b.Config.GetStringSlice("general." + key) | ||||
| 	} | ||||
| @@ -117,7 +127,7 @@ func (b *Bridge) GetStringSlice(key string) []string { | ||||
| } | ||||
|  | ||||
| func (b *Bridge) GetStringSlice2D(key string) [][]string { | ||||
| 	val, ok := b.Config.GetStringSlice2D(b.Account + "." + key) | ||||
| 	val, ok := b.Config.GetStringSlice2D(b.GetConfigKey(key)) | ||||
| 	if !ok { | ||||
| 		val, _ = b.Config.GetStringSlice2D("general." + key) | ||||
| 	} | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package config | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| @@ -22,11 +23,15 @@ const ( | ||||
| 	EventRejoinChannels    = "rejoin_channels" | ||||
| 	EventUserAction        = "user_action" | ||||
| 	EventMsgDelete         = "msg_delete" | ||||
| 	EventFileDelete        = "file_delete" | ||||
| 	EventAPIConnected      = "api_connected" | ||||
| 	EventUserTyping        = "user_typing" | ||||
| 	EventGetChannelMembers = "get_channel_members" | ||||
| 	EventNoticeIRC         = "notice_irc" | ||||
| ) | ||||
|  | ||||
| const ParentIDNotFound = "msg-parent-not-found" | ||||
|  | ||||
| type Message struct { | ||||
| 	Text      string    `json:"text"` | ||||
| 	Channel   string    `json:"channel"` | ||||
| @@ -43,14 +48,23 @@ type Message struct { | ||||
| 	Extra     map[string][]interface{} | ||||
| } | ||||
|  | ||||
| func (m Message) ParentNotFound() bool { | ||||
| 	return m.ParentID == ParentIDNotFound | ||||
| } | ||||
|  | ||||
| func (m Message) ParentValid() bool { | ||||
| 	return m.ParentID != "" && !m.ParentNotFound() | ||||
| } | ||||
|  | ||||
| type FileInfo struct { | ||||
| 	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 { | ||||
| @@ -73,36 +87,43 @@ 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 | ||||
| 	IconURL                string // mattermost, slack | ||||
| 	IgnoreFailureOnStart   bool   // general | ||||
| 	IgnoreNicks            string // all protocols | ||||
| 	IgnoreMessages         string // all protocols | ||||
| 	Jid                    string // xmpp | ||||
| 	Label                  string // all protocols | ||||
| 	Login                  string // mattermost, matrix | ||||
| 	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 | ||||
| 	MediaServerDownload    string | ||||
| 	MediaServerUpload      string | ||||
| 	MediaConvertTgs        string     // telegram | ||||
| 	MediaConvertWebPToPNG  bool       // telegram | ||||
| 	MessageDelay           int        // IRC, time in millisecond to wait between messages | ||||
| 	MessageFormat          string     // telegram | ||||
| 	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 | ||||
| @@ -112,7 +133,7 @@ type Protocol struct { | ||||
| 	NicksPerRow            int        // mattermost, slack | ||||
| 	NoHomeServerSuffix     bool       // matrix | ||||
| 	NoSendJoinPart         bool       // all protocols | ||||
| 	NoTLS                  bool       // mattermost | ||||
| 	NoTLS                  bool       // mattermost, xmpp | ||||
| 	Password               string     // IRC,mattermost,XMPP,matrix | ||||
| 	PrefixMessagesWithNick bool       // mattemost, slack | ||||
| 	PreserveThreading      bool       // slack | ||||
| @@ -120,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 | ||||
| @@ -134,12 +156,13 @@ type Protocol struct { | ||||
| 	SkipTLSVerify          bool       // IRC, mattermost | ||||
| 	SkipVersionCheck       bool       // mattermost | ||||
| 	StripNick              bool       // all protocols | ||||
| 	StripMarkdown          bool       // irc | ||||
| 	SyncTopic              bool       // slack | ||||
| 	TengoModifyMessage     string     // general | ||||
| 	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 | ||||
| @@ -148,8 +171,9 @@ type Protocol struct { | ||||
| 	UseTLS                 bool       // IRC | ||||
| 	UseDiscriminator       bool       // discord | ||||
| 	UseFirstName           bool       // telegram | ||||
| 	UseUserName            bool       // discord | ||||
| 	UseUserName            bool       // discord, matrix, mattermost | ||||
| 	UseInsecureURL         bool       // telegram | ||||
| 	UserName               string     // IRC | ||||
| 	VerboseJoinPart        bool       // IRC | ||||
| 	WebhookBindAddress     string     // mattermost, slack | ||||
| 	WebhookURL             string     // mattermost, slack | ||||
| @@ -207,6 +231,7 @@ type BridgeValues struct { | ||||
| 	WhatsApp           map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results | ||||
| 	Zulip              map[string]Protocol | ||||
| 	Keybase            map[string]Protocol | ||||
| 	Mumble             map[string]Protocol | ||||
| 	General            Protocol | ||||
| 	Tengo              Tengo | ||||
| 	Gateway            []Gateway | ||||
| @@ -216,6 +241,7 @@ type BridgeValues struct { | ||||
| type Config interface { | ||||
| 	Viper() *viper.Viper | ||||
| 	BridgeValues() *BridgeValues | ||||
| 	IsKeySet(key string) bool | ||||
| 	GetBool(key string) (bool, bool) | ||||
| 	GetInt(key string) (int, bool) | ||||
| 	GetString(key string) (string, bool) | ||||
| @@ -243,6 +269,15 @@ func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config { | ||||
|  | ||||
| 	cfgtype := detectConfigType(cfgfile) | ||||
| 	mycfg := newConfigFromString(logger, input, cfgtype) | ||||
| 	if mycfg.cv.General.LogFile != "" { | ||||
| 		logfile, err := os.OpenFile(mycfg.cv.General.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) | ||||
| 		if err == nil { | ||||
| 			logger.Info("Opening log file ", mycfg.cv.General.LogFile) | ||||
| 			rootLogger.Out = logfile | ||||
| 		} else { | ||||
| 			logger.Warn("Failed to open ", mycfg.cv.General.LogFile) | ||||
| 		} | ||||
| 	} | ||||
| 	if mycfg.cv.General.MediaDownloadSize == 0 { | ||||
| 		mycfg.cv.General.MediaDownloadSize = 1000000 | ||||
| 	} | ||||
| @@ -300,6 +335,12 @@ func (c *config) Viper() *viper.Viper { | ||||
| 	return c.v | ||||
| } | ||||
|  | ||||
| func (c *config) IsKeySet(key string) bool { | ||||
| 	c.RLock() | ||||
| 	defer c.RUnlock() | ||||
| 	return c.v.IsSet(key) | ||||
| } | ||||
|  | ||||
| func (c *config) GetBool(key string) (bool, bool) { | ||||
| 	c.RLock() | ||||
| 	defer c.RUnlock() | ||||
| @@ -359,6 +400,11 @@ type TestConfig struct { | ||||
| 	Overrides map[string]interface{} | ||||
| } | ||||
|  | ||||
| func (c *TestConfig) IsKeySet(key string) bool { | ||||
| 	_, ok := c.Overrides[key] | ||||
| 	return ok || c.Config.IsKeySet(key) | ||||
| } | ||||
|  | ||||
| func (c *TestConfig) GetBool(key string) (bool, bool) { | ||||
| 	val, ok := c.Overrides[key] | ||||
| 	if ok { | ||||
|   | ||||
| @@ -2,30 +2,31 @@ package bdiscord | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"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 | ||||
|  | ||||
| 	c *discordgo.Session | ||||
|  | ||||
| 	nick            string | ||||
| 	userID          string | ||||
| 	guildID         string | ||||
| 	webhookID       string | ||||
| 	webhookToken    string | ||||
| 	canEditWebhooks bool | ||||
| 	nick    string | ||||
| 	userID  string | ||||
| 	guildID string | ||||
|  | ||||
| 	channelsMutex  sync.RWMutex | ||||
| 	channels       []*discordgo.Channel | ||||
| @@ -34,30 +35,39 @@ type Bdiscord struct { | ||||
| 	membersMutex  sync.RWMutex | ||||
| 	userMemberMap map[string]*discordgo.Member | ||||
| 	nickMemberMap map[string]*discordgo.Member | ||||
|  | ||||
| 	// 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) | ||||
| 	if b.GetString("WebhookURL") != "" { | ||||
| 		b.Log.Debug("Configuring Discord Incoming Webhook") | ||||
| 		b.webhookID, b.webhookToken = b.splitURL(b.GetString("WebhookURL")) | ||||
|  | ||||
| 	b.useAutoWebhooks = b.GetBool("AutoWebhooks") | ||||
| 	if b.useAutoWebhooks { | ||||
| 		b.Log.Debug("Using automatic webhooks") | ||||
| 	} | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) Connect() error { | ||||
| 	var err error | ||||
| 	var guildFound bool | ||||
| 	token := b.GetString("Token") | ||||
| 	b.Log.Info("Connecting") | ||||
| 	if b.GetString("WebhookURL") == "" { | ||||
| 		b.Log.Info("Connecting using token") | ||||
| 	} else { | ||||
| 		b.Log.Info("Connecting using webhookurl (for posting) and token") | ||||
| 	} | ||||
| 	if !strings.HasPrefix(b.GetString("Token"), "Bot ") { | ||||
| 		token = "Bot " + b.GetString("Token") | ||||
| 	} | ||||
| @@ -71,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 | ||||
| 	} | ||||
| @@ -94,62 +101,107 @@ func (b *Bdiscord) Connect() error { | ||||
| 	serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1) | ||||
| 	b.nick = userinfo.Username | ||||
| 	b.userID = userinfo.ID | ||||
|  | ||||
| 	// Try and find this account's guild, and populate channels | ||||
| 	b.channelsMutex.Lock() | ||||
| 	for _, guild := range guilds { | ||||
| 		if guild.Name == serverName || guild.ID == serverName { | ||||
| 			b.channels, err = b.c.GuildChannels(guild.ID) | ||||
| 			if err != nil { | ||||
| 				break | ||||
| 			} | ||||
| 			b.guildID = guild.ID | ||||
| 			guildFound = true | ||||
| 		// Skip, if the server name does not match the visible name or the ID | ||||
| 		if guild.Name != serverName && guild.ID != serverName { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// Complain about an ambiguous Server setting. Two Discord servers could have the same title! | ||||
| 		// For IDs, practically this will never happen. It would only trigger if some server's name is also an ID. | ||||
| 		if b.guildID != "" { | ||||
| 			return fmt.Errorf("found multiple Discord servers with the same name %#v, expected to see only one", serverName) | ||||
| 		} | ||||
|  | ||||
| 		// Getting this guild's channel could result in a permission error | ||||
| 		b.channels, err = b.c.GuildChannels(guild.ID) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("could not get %#v's channels: %w", b.GetString("Server"), err) | ||||
| 		} | ||||
|  | ||||
| 		b.guildID = guild.ID | ||||
| 	} | ||||
| 	b.channelsMutex.Unlock() | ||||
| 	if !guildFound { | ||||
| 		msg := fmt.Sprintf("Server \"%s\" not found", b.GetString("Server")) | ||||
| 		err = errors.New(msg) | ||||
| 		b.Log.Error(msg) | ||||
| 		b.Log.Info("Possible values:") | ||||
|  | ||||
| 	// If we couldn't find a guild, we print extra debug information and return a nice error | ||||
| 	if b.guildID == "" { | ||||
| 		err = fmt.Errorf("could not find Discord server %#v", b.GetString("Server")) | ||||
| 		b.Log.Error(err.Error()) | ||||
|  | ||||
| 		// Print all of the possible server values | ||||
| 		b.Log.Info("Possible server values:") | ||||
| 		for _, guild := range guilds { | ||||
| 			b.Log.Infof("Server=\"%s\" # Server name", guild.Name) | ||||
| 			b.Log.Infof("Server=\"%s\" # Server ID", guild.ID) | ||||
| 			b.Log.Infof("\t- Server=%#v # by name", guild.Name) | ||||
| 			b.Log.Infof("\t- Server=%#v # by ID", guild.ID) | ||||
| 		} | ||||
| 	} | ||||
| 	if err != nil { | ||||
|  | ||||
| 		// If there are no results, we should say that | ||||
| 		if len(guilds) == 0 { | ||||
| 			b.Log.Info("\t- (none found)") | ||||
| 		} | ||||
|  | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	b.channelsMutex.RLock() | ||||
| 	if b.GetString("WebhookURL") == "" { | ||||
| 		for _, channel := range b.channels { | ||||
| 			b.Log.Debugf("found channel %#v", channel) | ||||
| 		} | ||||
| 	} else { | ||||
| 		manageWebhooks := discordgo.PermissionManageWebhooks | ||||
| 		var channelsDenied []string | ||||
| 		for _, info := range b.Channels { | ||||
| 			id := b.getChannelID(info.Name) // note(qaisjp): this readlocks channelsMutex | ||||
| 			b.Log.Debugf("Verifying PermissionManageWebhooks for %s with ID %s", info.ID, id) | ||||
| 	// Legacy note: WebhookURL used to have an actual webhook URL that we would edit, | ||||
| 	// but we stopped doing that due to Discord making rate limits more aggressive. | ||||
| 	// | ||||
| 	// Even older: the same WebhookURL used to be used by every channel, which is usually unexpected. | ||||
| 	// This is no longer possible. | ||||
| 	if b.GetString("WebhookURL") != "" { | ||||
| 		message := "The global WebhookURL setting has been removed. " | ||||
| 		message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. " | ||||
| 		message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections." | ||||
| 		b.Log.Errorln(message) | ||||
| 		return fmt.Errorf("use of removed WebhookURL setting") | ||||
| 	} | ||||
|  | ||||
| 			perms, permsErr := b.c.UserChannelPermissions(userinfo.ID, id) | ||||
| 			if permsErr != nil { | ||||
| 				b.Log.Warnf("Failed to check PermissionManageWebhooks in channel \"%s\": %s", info.Name, permsErr.Error()) | ||||
| 			} else if perms&manageWebhooks == manageWebhooks { | ||||
| 				continue | ||||
| 	if b.GetInt("debuglevel") == 2 { | ||||
| 		b.Log.Debug("enabling even more discord debug") | ||||
| 		b.c.Debug = true | ||||
| 	} | ||||
|  | ||||
| 	// Initialise webhook management | ||||
| 	b.transmitter = transmitter.New(b.c, b.guildID, "matterbridge", b.useAutoWebhooks) | ||||
| 	b.transmitter.Log = b.Log | ||||
|  | ||||
| 	var webhookChannelIDs []string | ||||
| 	for _, channel := range b.Channels { | ||||
| 		channelID := b.getChannelID(channel.Name) // note(qaisjp): this readlocks channelsMutex | ||||
|  | ||||
| 		// If a WebhookURL was not explicitly provided for this channel, | ||||
| 		// there are two options: just a regular bot message (ugly) or this is should be webhook sent | ||||
| 		if channel.Options.WebhookURL == "" { | ||||
| 			// If it should be webhook sent, we should enforce this via the transmitter | ||||
| 			if b.useAutoWebhooks { | ||||
| 				webhookChannelIDs = append(webhookChannelIDs, channelID) | ||||
| 			} | ||||
| 			channelsDenied = append(channelsDenied, fmt.Sprintf("%#v", info.Name)) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		b.canEditWebhooks = len(channelsDenied) == 0 | ||||
| 		if b.canEditWebhooks { | ||||
| 			b.Log.Info("Can manage webhooks; will edit channel for global webhook on send") | ||||
| 		} else { | ||||
| 			b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send") | ||||
| 			b.Log.Warn("Can't manage webhooks in channels: ", strings.Join(channelsDenied, ", ")) | ||||
| 		whID, whToken, ok := b.splitURL(channel.Options.WebhookURL) | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("failed to parse WebhookURL %#v for channel %#v", channel.Options.WebhookURL, channel.ID) | ||||
| 		} | ||||
|  | ||||
| 		b.transmitter.AddWebhook(channelID, &discordgo.Webhook{ | ||||
| 			ID:        whID, | ||||
| 			Token:     whToken, | ||||
| 			GuildID:   b.guildID, | ||||
| 			ChannelID: channelID, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	if b.useAutoWebhooks { | ||||
| 		err = b.transmitter.RefreshGuildWebhooks(webhookChannelIDs) | ||||
| 		if err != nil { | ||||
| 			b.Log.WithError(err).Println("transmitter could not refresh guild webhooks") | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	b.channelsMutex.RUnlock() | ||||
|  | ||||
| 	// Obtaining guild members and initializing nickname mapping. | ||||
| 	b.membersMutex.Lock() | ||||
| @@ -170,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 | ||||
| } | ||||
|  | ||||
| @@ -206,76 +271,22 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { | ||||
| 		msg.Text = "_" + msg.Text + "_" | ||||
| 	} | ||||
|  | ||||
| 	// use initial webhook configured for the entire Discord account | ||||
| 	isGlobalWebhook := true | ||||
| 	wID := b.webhookID | ||||
| 	wToken := b.webhookToken | ||||
|  | ||||
| 	// check if have a channel specific webhook | ||||
| 	b.channelsMutex.RLock() | ||||
| 	if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok { | ||||
| 		if ci.Options.WebhookURL != "" { | ||||
| 			wID, wToken = b.splitURL(ci.Options.WebhookURL) | ||||
| 			isGlobalWebhook = false | ||||
| 		} | ||||
| 	// Handle prefix hint for unthreaded messages. | ||||
| 	if msg.ParentNotFound() { | ||||
| 		msg.ParentID = "" | ||||
| 	} | ||||
| 	b.channelsMutex.RUnlock() | ||||
|  | ||||
| 	// Use webhook to send the message | ||||
| 	if wID != "" && msg.Event != config.EventMsgDelete { | ||||
| 		// skip events | ||||
| 		if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange { | ||||
| 			return "", nil | ||||
| 		} | ||||
|  | ||||
| 		// If we are editing a message, delete the old message | ||||
| 		if msg.ID != "" { | ||||
| 			b.Log.Debugf("Deleting edited webhook message") | ||||
| 			err := b.c.ChannelMessageDelete(channelID, msg.ID) | ||||
| 			if err != nil { | ||||
| 				b.Log.Errorf("Could not delete edited webhook message: %s", err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		b.Log.Debugf("Broadcasting using Webhook") | ||||
|  | ||||
| 		// skip empty messages | ||||
| 		if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) { | ||||
| 			b.Log.Debugf("Skipping empty message %#v", msg) | ||||
| 			return "", nil | ||||
| 		} | ||||
|  | ||||
| 		msg.Text = helper.ClipMessage(msg.Text, MessageLength) | ||||
| 		msg.Text = b.replaceUserMentions(msg.Text) | ||||
| 		// discord username must be [0..32] max | ||||
| 		if len(msg.Username) > 32 { | ||||
| 			msg.Username = msg.Username[0:32] | ||||
| 		} | ||||
| 		// if we have a global webhook for this Discord account, and permission | ||||
| 		// to modify webhooks (previously verified), then set its channel to | ||||
| 		// the message channel before using it | ||||
| 		// TODO: this isn't necessary if the last message from this webhook was | ||||
| 		// sent to the current channel | ||||
| 		if isGlobalWebhook && b.canEditWebhooks { | ||||
| 			b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel) | ||||
| 			_, err := b.c.WebhookEdit(wID, "", "", channelID) | ||||
| 			if err != nil { | ||||
| 				b.Log.Errorf("Could not set webhook channel: %s", err) | ||||
| 				return "", err | ||||
| 			} | ||||
| 		} | ||||
| 		b.Log.Debugf("Processing webhook sending for message %#v", msg) | ||||
| 		msg, err := b.webhookSend(&msg, wID, wToken) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("Could not broadcast via webook for message %#v: %s", msg, err) | ||||
| 			return "", err | ||||
| 		} | ||||
| 		if msg == nil { | ||||
| 			return "", nil | ||||
| 		} | ||||
| 		return msg.ID, nil | ||||
| 	useWebhooks := b.shouldMessageUseWebhooks(&msg) | ||||
| 	if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" { | ||||
| 		return b.handleEventWebhook(&msg, channelID) | ||||
| 	} | ||||
|  | ||||
| 	return b.handleEventBotUser(&msg, channelID) | ||||
| } | ||||
|  | ||||
| // handleEventDirect handles events via the bot user | ||||
| func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) { | ||||
| 	b.Log.Debugf("Broadcasting using token (API)") | ||||
|  | ||||
| 	// Delete message | ||||
| @@ -287,80 +298,87 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { | ||||
| 		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) | ||||
| 		for _, rmsg := range helper.HandleExtra(msg, b.General) { | ||||
| 			// 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) | ||||
| 			} | ||||
| 		} | ||||
| 		// check if we have files to upload (from slack, telegram or mattermost) | ||||
| 		if len(msg.Extra["file"]) > 0 { | ||||
| 			return b.handleUploadFile(&msg, channelID) | ||||
| 			return b.handleUploadFile(msg, channelID) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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 | ||||
| 	} | ||||
|  | ||||
| 	// Post normal message | ||||
| 	res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return res.ID, nil | ||||
| } | ||||
|  | ||||
| // useWebhook returns true if we have a webhook defined somewhere | ||||
| func (b *Bdiscord) useWebhook() bool { | ||||
| 	if b.GetString("WebhookURL") != "" { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	b.channelsMutex.RLock() | ||||
| 	defer b.channelsMutex.RUnlock() | ||||
|  | ||||
| 	for _, channel := range b.channelInfoMap { | ||||
| 		if channel.Options.WebhookURL != "" { | ||||
| 			return true | ||||
| 		// 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))") | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // isWebhookID returns true if the specified id is used in a defined webhook | ||||
| func (b *Bdiscord) isWebhookID(id string) bool { | ||||
| 	if b.GetString("WebhookURL") != "" { | ||||
| 		wID, _ := b.splitURL(b.GetString("WebhookURL")) | ||||
| 		if wID == id { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	b.channelsMutex.RLock() | ||||
| 	defer b.channelsMutex.RUnlock() | ||||
|  | ||||
| 	for _, channel := range b.channelInfoMap { | ||||
| 		if channel.Options.WebhookURL != "" { | ||||
| 			wID, _ := b.splitURL(channel.Options.WebhookURL) | ||||
| 			if wID == id { | ||||
| 				return true | ||||
| 		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 | ||||
| 	} | ||||
| 	return false | ||||
|  | ||||
| 	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) | ||||
| 	} | ||||
|  | ||||
| 	// 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{ | ||||
| @@ -369,93 +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 | ||||
| } | ||||
|  | ||||
| // 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, webhookID, token string) (*discordgo.Message, error) { | ||||
| 	var ( | ||||
| 		res *discordgo.Message | ||||
| 		err error | ||||
| 	) | ||||
|  | ||||
| 	// If avatar is unset, check if UseLocalAvatar contains the message's | ||||
| 	// account or protocol, and if so, try to find a local avatar | ||||
| 	if msg.Avatar == "" { | ||||
| 		for _, val := range b.GetStringSlice("UseLocalAvatar") { | ||||
| 			if msg.Protocol == val || msg.Account == val { | ||||
| 				if avatar := b.findAvatar(msg); avatar != "" { | ||||
| 					msg.Avatar = avatar | ||||
| 				} | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// WebhookParams can have either `Content` or `File`. | ||||
|  | ||||
| 	// We can't send empty messages. | ||||
| 	if msg.Text != "" { | ||||
| 		res, err = b.c.WebhookExecute( | ||||
| 			webhookID, | ||||
| 			token, | ||||
| 			true, | ||||
| 			&discordgo.WebhookParams{ | ||||
| 				Content:   msg.Text, | ||||
| 				Username:  msg.Username, | ||||
| 				AvatarURL: msg.Avatar, | ||||
| 			}, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if msg.Extra != nil { | ||||
| 		for _, f := range msg.Extra["file"] { | ||||
| 			fi := f.(config.FileInfo) | ||||
| 			file := discordgo.File{ | ||||
| 				Name:        fi.Name, | ||||
| 				ContentType: "", | ||||
| 				Reader:      bytes.NewReader(*fi.Data), | ||||
| 			} | ||||
| 			content := "" | ||||
| 			if msg.Text == "" { | ||||
| 				content = fi.Comment | ||||
| 			} | ||||
| 			_, e2 := b.c.WebhookExecute( | ||||
| 				webhookID, | ||||
| 				token, | ||||
| 				false, | ||||
| 				&discordgo.WebhookParams{ | ||||
| 					Username:  msg.Username, | ||||
| 					AvatarURL: msg.Avatar, | ||||
| 					File:      &file, | ||||
| 					Content:   content, | ||||
| 				}, | ||||
| 			) | ||||
| 			if e2 != nil { | ||||
| 				b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return res, err | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) findAvatar(m *config.Message) string { | ||||
| 	member, err := b.getGuildMemberByNick(m.Username) | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return member.User.AvatarURL("") | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| @@ -69,7 +94,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat | ||||
| 		return | ||||
| 	} | ||||
| 	// if using webhooks, do not relay if it's ours | ||||
| 	if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) { | ||||
| 	if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -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 { | ||||
| @@ -127,12 +153,21 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat | ||||
| 	// Replace emotes | ||||
| 	rmsg.Text = replaceEmotes(rmsg.Text) | ||||
|  | ||||
| 	// Add our parent id if it exists, and if it's not referring to a message in another channel | ||||
| 	if ref := m.MessageReference; ref != nil && ref.ChannelID == m.ChannelID { | ||||
| 		rmsg.ParentID = ref.MessageID | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account) | ||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||
| 	b.Remote <- rmsg | ||||
| } | ||||
|  | ||||
| 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) | ||||
| 	} | ||||
| @@ -160,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 | ||||
| @@ -181,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 | ||||
| @@ -217,7 +266,7 @@ func handleEmbed(embed *discordgo.MessageEmbed) string { | ||||
|  | ||||
| 		i++ | ||||
| 		if i == 1 { | ||||
| 			result += "embed: " + e | ||||
| 			result += " embed: " + e | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ package bdiscord | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/matterbridge/discordgo" | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| @@ -20,14 +20,14 @@ func TestHandleEmbed(t *testing.T) { | ||||
| 			embed: &discordgo.MessageEmbed{ | ||||
| 				Title: "blah", | ||||
| 			}, | ||||
| 			result: "embed: blah\n", | ||||
| 			result: " embed: blah\n", | ||||
| 		}, | ||||
| 		"two": { | ||||
| 			embed: &discordgo.MessageEmbed{ | ||||
| 				Title:       "blah", | ||||
| 				Description: "blah2", | ||||
| 			}, | ||||
| 			result: "embed: blah - blah2\n", | ||||
| 			result: " embed: blah - blah2\n", | ||||
| 		}, | ||||
| 		"three": { | ||||
| 			embed: &discordgo.MessageEmbed{ | ||||
| @@ -35,20 +35,20 @@ func TestHandleEmbed(t *testing.T) { | ||||
| 				Description: "blah2", | ||||
| 				URL:         "blah3", | ||||
| 			}, | ||||
| 			result: "embed: blah - blah2 - blah3\n", | ||||
| 			result: " embed: blah - blah2 - blah3\n", | ||||
| 		}, | ||||
| 		"twob": { | ||||
| 			embed: &discordgo.MessageEmbed{ | ||||
| 				Description: "blah2", | ||||
| 				URL:         "blah3", | ||||
| 			}, | ||||
| 			result: "embed: blah2 - blah3\n", | ||||
| 			result: " embed: blah2 - blah3\n", | ||||
| 		}, | ||||
| 		"oneb": { | ||||
| 			embed: &discordgo.MessageEmbed{ | ||||
| 				URL: "blah3", | ||||
| 			}, | ||||
| 			result: "embed: blah3\n", | ||||
| 			result: " embed: blah3\n", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| @@ -188,14 +212,15 @@ func replaceEmotes(text string) string { | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) replaceAction(text string) (string, bool) { | ||||
| 	if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") { | ||||
| 		return text[1 : len(text)-1], true | ||||
| 	length := len(text) | ||||
| 	if length > 1 && text[0] == '_' && text[length-1] == '_' { | ||||
| 		return text[1 : length-1], true | ||||
| 	} | ||||
| 	return text, false | ||||
| } | ||||
|  | ||||
| // splitURL splits a webhookURL and returns the ID and token. | ||||
| func (b *Bdiscord) splitURL(url string) (string, string) { | ||||
| func (b *Bdiscord) splitURL(url string) (string, string, bool) { | ||||
| 	const ( | ||||
| 		expectedWebhookSplitCount = 7 | ||||
| 		webhookIdxID              = 5 | ||||
| @@ -203,9 +228,9 @@ func (b *Bdiscord) splitURL(url string) (string, string) { | ||||
| 	) | ||||
| 	webhookURLSplit := strings.Split(url, "/") | ||||
| 	if len(webhookURLSplit) != expectedWebhookSplitCount { | ||||
| 		b.Log.Fatalf("%s is no correct discord WebhookURL", url) | ||||
| 		return "", "", false | ||||
| 	} | ||||
| 	return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken] | ||||
| 	return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken], true | ||||
| } | ||||
|  | ||||
| func enumerateUsernames(s string) []string { | ||||
|   | ||||
							
								
								
									
										257
									
								
								bridge/discord/transmitter/transmitter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,257 @@ | ||||
| // Package transmitter provides functionality for transmitting | ||||
| // arbitrary webhook messages to Discord. | ||||
| // | ||||
| // The package provides the following functionality: | ||||
| // | ||||
| // - Creating new webhooks, whenever necessary | ||||
| // - Loading webhooks that we have previously created | ||||
| // - Sending new messages | ||||
| // - Editing messages, via message ID | ||||
| // - Deleting messages, via message ID | ||||
| // | ||||
| // The package has been designed for matterbridge, but with other | ||||
| // Go bots in mind. The public API should be matterbridge-agnostic. | ||||
| package transmitter | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| // A Transmitter represents a message manager for a single guild. | ||||
| type Transmitter struct { | ||||
| 	session    *discordgo.Session | ||||
| 	guild      string | ||||
| 	title      string | ||||
| 	autoCreate bool | ||||
|  | ||||
| 	// channelWebhooks maps from a channel ID to a webhook instance | ||||
| 	channelWebhooks map[string]*discordgo.Webhook | ||||
|  | ||||
| 	mutex sync.RWMutex | ||||
|  | ||||
| 	Log *log.Entry | ||||
| } | ||||
|  | ||||
| // ErrWebhookNotFound is returned when a valid webhook for this channel/message combination does not exist | ||||
| var ErrWebhookNotFound = errors.New("webhook for this channel and message does not exist") | ||||
|  | ||||
| // ErrPermissionDenied is returned if the bot does not have permission to manage webhooks. | ||||
| // | ||||
| // Bots can be granted a guild-wide permission and channel-specific permissions to manage webhooks. | ||||
| // Despite potentially having guild-wide permission, channel specific overrides could deny a bot's permission to manage webhooks. | ||||
| var ErrPermissionDenied = errors.New("missing 'Manage Webhooks' permission") | ||||
|  | ||||
| // New returns a new Transmitter given a Discord session, guild ID, and title. | ||||
| func New(session *discordgo.Session, guild string, title string, autoCreate bool) *Transmitter { | ||||
| 	return &Transmitter{ | ||||
| 		session:    session, | ||||
| 		guild:      guild, | ||||
| 		title:      title, | ||||
| 		autoCreate: autoCreate, | ||||
|  | ||||
| 		channelWebhooks: make(map[string]*discordgo.Webhook), | ||||
|  | ||||
| 		Log: log.NewEntry(log.StandardLogger()), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Send transmits a message to the given channel with the provided webhook data, and waits until Discord responds with message data. | ||||
| func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) { | ||||
| 	wh, err := t.getOrCreateWebhook(channelID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	msg, err := t.session.WebhookExecute(wh.ID, wh.Token, true, params) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("execute failed: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return msg, nil | ||||
| } | ||||
|  | ||||
| // Edit will edit a message in a channel, if possible. | ||||
| func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo.WebhookParams) error { | ||||
| 	wh := t.getWebhook(channelID) | ||||
|  | ||||
| 	if wh == nil { | ||||
| 		return ErrWebhookNotFound | ||||
| 	} | ||||
|  | ||||
| 	uri := discordgo.EndpointWebhookToken(wh.ID, wh.Token) + "/messages/" + messageID | ||||
| 	_, err := t.session.RequestWithBucketID("PATCH", uri, params, discordgo.EndpointWebhookToken("", "")) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // HasWebhook checks whether the transmitter is using a particular webhook. | ||||
| func (t *Transmitter) HasWebhook(id string) bool { | ||||
| 	t.mutex.RLock() | ||||
| 	defer t.mutex.RUnlock() | ||||
|  | ||||
| 	for _, wh := range t.channelWebhooks { | ||||
| 		if wh.ID == id { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // AddWebhook allows you to register a channel's webhook with the transmitter. | ||||
| func (t *Transmitter) AddWebhook(channelID string, webhook *discordgo.Webhook) bool { | ||||
| 	t.Log.Debugf("Manually added webhook %#v to channel %#v", webhook.ID, channelID) | ||||
| 	t.mutex.Lock() | ||||
| 	defer t.mutex.Unlock() | ||||
|  | ||||
| 	_, replaced := t.channelWebhooks[channelID] | ||||
| 	t.channelWebhooks[channelID] = webhook | ||||
| 	return replaced | ||||
| } | ||||
|  | ||||
| // RefreshGuildWebhooks loads "relevant" webhooks into the transmitter, with careful permission handling. | ||||
| // | ||||
| // Notes: | ||||
| // | ||||
| // - A webhook is "relevant" if it was created by this bot -- the ApplicationID should match the bot's ID. | ||||
| // - The term "having permission" means having the "Manage Webhooks" permission. See ErrPermissionDenied for more information. | ||||
| // - This function is additive and will not unload previously loaded webhooks. | ||||
| // - A nil channelIDs slice is treated the same as an empty one. | ||||
| // | ||||
| // If the bot has guild-wide permission: | ||||
| // | ||||
| // 1. it will load any "relevant" webhooks from the entire guild | ||||
| // 2. the given slice is ignored | ||||
| // | ||||
| // If the bot does not have guild-wide permission: | ||||
| // | ||||
| // 1. it will load any "relevant" webhooks in each channel | ||||
| // 2. a single error will be returned if any error occurs (incl. if there is no permission for any of these channels) | ||||
| // | ||||
| // If any channel has more than one "relevant" webhook, it will randomly pick one. | ||||
| func (t *Transmitter) RefreshGuildWebhooks(channelIDs []string) error { | ||||
| 	t.Log.Debugln("Refreshing guild webhooks") | ||||
|  | ||||
| 	botID, err := getDiscordUserID(t.session) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("could not get current user: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Get all existing webhooks | ||||
| 	hooks, err := t.session.GuildWebhooks(t.guild) | ||||
| 	if err != nil { | ||||
| 		switch { | ||||
| 		case isDiscordPermissionError(err): | ||||
| 			// We fallback on manually fetching hooks from individual channels | ||||
| 			// if we don't have the "Manage Webhooks" permission globally. | ||||
| 			// We can only do this if we were provided channelIDs, though. | ||||
| 			if len(channelIDs) == 0 { | ||||
| 				return ErrPermissionDenied | ||||
| 			} | ||||
| 			t.Log.Debugln("Missing global 'Manage Webhooks' permission, falling back on per-channel permission") | ||||
| 			return t.fetchChannelsHooks(channelIDs, botID) | ||||
| 		default: | ||||
| 			return fmt.Errorf("could not get webhooks: %w", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	t.Log.Debugln("Refreshing guild webhooks using global permission") | ||||
| 	t.assignHooksByAppID(hooks, botID, false) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // createWebhook creates a webhook for a specific channel. | ||||
| func (t *Transmitter) createWebhook(channel string) (*discordgo.Webhook, error) { | ||||
| 	t.mutex.Lock() | ||||
| 	defer t.mutex.Unlock() | ||||
|  | ||||
| 	wh, err := t.session.WebhookCreate(channel, t.title+time.Now().Format(" 3:04:05PM"), "") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	t.channelWebhooks[channel] = wh | ||||
| 	return wh, nil | ||||
| } | ||||
|  | ||||
| func (t *Transmitter) getWebhook(channel string) *discordgo.Webhook { | ||||
| 	t.mutex.RLock() | ||||
| 	defer t.mutex.RUnlock() | ||||
|  | ||||
| 	return t.channelWebhooks[channel] | ||||
| } | ||||
|  | ||||
| func (t *Transmitter) getOrCreateWebhook(channelID string) (*discordgo.Webhook, error) { | ||||
| 	// If we have a webhook for this channel, immediately return it | ||||
| 	wh := t.getWebhook(channelID) | ||||
| 	if wh != nil { | ||||
| 		return wh, nil | ||||
| 	} | ||||
|  | ||||
| 	// Early exit if we don't want to automatically create one | ||||
| 	if !t.autoCreate { | ||||
| 		return nil, ErrWebhookNotFound | ||||
| 	} | ||||
|  | ||||
| 	t.Log.Infof("Creating a webhook for %s\n", channelID) | ||||
| 	wh, err := t.createWebhook(channelID) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("could not create webhook: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return wh, nil | ||||
| } | ||||
|  | ||||
| // fetchChannelsHooks fetches hooks for the given channelIDs and calls assignHooksByAppID for each channel's hooks | ||||
| func (t *Transmitter) fetchChannelsHooks(channelIDs []string, botID string) error { | ||||
| 	// For each channel, search for relevant hooks | ||||
| 	var failedHooks []string | ||||
| 	for _, channelID := range channelIDs { | ||||
| 		hooks, err := t.session.ChannelWebhooks(channelID) | ||||
| 		if err != nil { | ||||
| 			failedHooks = append(failedHooks, "\n- "+channelID+": "+err.Error()) | ||||
| 			continue | ||||
| 		} | ||||
| 		t.assignHooksByAppID(hooks, botID, true) | ||||
| 	} | ||||
|  | ||||
| 	// Compose an error if any hooks failed | ||||
| 	if len(failedHooks) > 0 { | ||||
| 		return errors.New("failed to fetch hooks:" + strings.Join(failedHooks, "")) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t *Transmitter) assignHooksByAppID(hooks []*discordgo.Webhook, appID string, channelTargeted bool) { | ||||
| 	logLine := "Picking up webhook" | ||||
| 	if channelTargeted { | ||||
| 		logLine += " (channel targeted)" | ||||
| 	} | ||||
|  | ||||
| 	t.mutex.Lock() | ||||
| 	defer t.mutex.Unlock() | ||||
|  | ||||
| 	for _, wh := range hooks { | ||||
| 		if wh.ApplicationID != appID { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		t.channelWebhooks[wh.ChannelID] = wh | ||||
| 		t.Log.WithFields(log.Fields{ | ||||
| 			"id":      wh.ID, | ||||
| 			"name":    wh.Name, | ||||
| 			"channel": wh.ChannelID, | ||||
| 		}).Println(logLine) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										32
									
								
								bridge/discord/transmitter/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,32 @@ | ||||
| package transmitter | ||||
|  | ||||
| import ( | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| ) | ||||
|  | ||||
| // isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions | ||||
| func isDiscordPermissionError(err error) bool { | ||||
| 	if err == nil { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	restErr, ok := err.(*discordgo.RESTError) | ||||
| 	if !ok { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return restErr.Message != nil && restErr.Message.Code == discordgo.ErrCodeMissingPermissions | ||||
| } | ||||
|  | ||||
| // getDiscordUserID gets own user ID from state, and fallback on API request | ||||
| func getDiscordUserID(session *discordgo.Session) (string, error) { | ||||
| 	if user := session.State.User; user != nil { | ||||
| 		return user.ID, nil | ||||
| 	} | ||||
|  | ||||
| 	user, err := session.User("@me") | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return user.ID, nil | ||||
| } | ||||
							
								
								
									
										179
									
								
								bridge/discord/webhook.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,179 @@ | ||||
| package bdiscord | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/bwmarrin/discordgo" | ||||
| ) | ||||
|  | ||||
| // shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks | ||||
| func (b *Bdiscord) shouldMessageUseWebhooks(msg *config.Message) bool { | ||||
| 	if b.useAutoWebhooks { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	b.channelsMutex.RLock() | ||||
| 	defer b.channelsMutex.RUnlock() | ||||
| 	if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok { | ||||
| 		if ci.Options.WebhookURL != "" { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // maybeGetLocalAvatar checks if UseLocalAvatar contains the message's | ||||
| // account or protocol, and if so, returns the Discord avatar (if exists) | ||||
| func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string { | ||||
| 	for _, val := range b.GetStringSlice("UseLocalAvatar") { | ||||
| 		if msg.Protocol != val && msg.Account != val { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		member, err := b.getGuildMemberByNick(msg.Username) | ||||
| 		if err != nil { | ||||
| 			return "" | ||||
| 		} | ||||
|  | ||||
| 		return member.User.AvatarURL("") | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| 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) (string, error) { | ||||
| 	var ( | ||||
| 		res string | ||||
| 		err error | ||||
| 	) | ||||
|  | ||||
| 	// If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this) | ||||
| 	if msg.Avatar == "" { | ||||
| 		msg.Avatar = b.maybeGetLocalAvatar(msg) | ||||
| 	} | ||||
|  | ||||
| 	// WebhookParams can have either `Content` or `File`. | ||||
|  | ||||
| 	// We can't send empty messages. | ||||
| 	if msg.Text != "" { | ||||
| 		res, err = b.webhookSendTextOnly(msg, channelID) | ||||
| 	} | ||||
|  | ||||
| 	if err == nil && msg.Extra != nil { | ||||
| 		err = b.webhookSendFilesOnly(msg, channelID) | ||||
| 	} | ||||
|  | ||||
| 	return res, err | ||||
| } | ||||
|  | ||||
| func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (string, error) { | ||||
| 	// skip events | ||||
| 	if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	// skip empty messages | ||||
| 	if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) { | ||||
| 		b.Log.Debugf("Skipping empty message %#v", msg) | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	// 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") | ||||
| 		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): %s; sending as new message(s) instead", editErr) | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("Processing webhook sending for message %#v", msg) | ||||
| 	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", msgID, err) | ||||
| 		return "", err | ||||
| 	} | ||||
| 	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 | ||||
| } | ||||
| @@ -48,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. | ||||
| @@ -55,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 | ||||
| @@ -142,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, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| @@ -166,21 +204,61 @@ 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 | ||||
| 	extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode | ||||
| 	markdownParser := parser.NewWithExtensions(extensions) | ||||
| 	renderer := html.NewRenderer(html.RendererOptions{ | ||||
| 		Flags: 0, | ||||
| @@ -192,7 +270,7 @@ func ParseMarkdown(input string) string { | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| // ConvertWebPToPNG convert input data (which should be WebP format to PNG format) | ||||
| // ConvertWebPToPNG converts input data (which should be WebP format) to PNG format | ||||
| func ConvertWebPToPNG(data *[]byte) error { | ||||
| 	r := bytes.NewReader(*data) | ||||
| 	m, err := webp.Decode(r) | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
| @@ -10,7 +10,6 @@ import ( | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/dfordsoft/golib/ic" | ||||
| 	"github.com/lrstanley/girc" | ||||
| 	"github.com/paulrosania/go-charset/charset" | ||||
| 	"github.com/saintfish/chardet" | ||||
| @@ -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) | ||||
| @@ -54,12 +53,12 @@ func (b *Birc) handleFiles(msg *config.Message) bool { | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi := f.(config.FileInfo) | ||||
| 		if fi.Comment != "" { | ||||
| 			msg.Text += fi.Comment + ": " | ||||
| 			msg.Text += fi.Comment + " : " | ||||
| 		} | ||||
| 		if fi.URL != "" { | ||||
| 			msg.Text = fi.URL | ||||
| 			if fi.Comment != "" { | ||||
| 				msg.Text = fi.Comment + ": " + fi.URL | ||||
| 				msg.Text = fi.Comment + " : " + fi.URL | ||||
| 			} | ||||
| 		} | ||||
| 		b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event} | ||||
| @@ -67,6 +66,20 @@ func (b *Birc) handleFiles(msg *config.Message) bool { | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (b *Birc) handleInvite(client *girc.Client, event girc.Event) { | ||||
| 	if len(event.Params) != 2 { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	channel := event.Params[1] | ||||
|  | ||||
| 	b.Log.Debugf("got invite for %s", channel) | ||||
|  | ||||
| 	if _, ok := b.channels[channel]; ok { | ||||
| 		b.i.Cmd.Join(channel) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) { | ||||
| 	if len(event.Params) == 0 { | ||||
| 		b.Log.Debugf("handleJoinPart: empty Params? %#v", event) | ||||
| @@ -109,14 +122,25 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) { | ||||
| 	i := b.i | ||||
| 	b.Nick = event.Params[0] | ||||
|  | ||||
| 	i.Handlers.Add("PRIVMSG", b.handlePrivMsg) | ||||
| 	i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg) | ||||
| 	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.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) | ||||
| 	i.Handlers.Add(girc.NOTICE, b.handleNotice) | ||||
| 	i.Handlers.Add("JOIN", b.handleJoinPart) | ||||
| 	i.Handlers.Add("PART", b.handleJoinPart) | ||||
| 	i.Handlers.Add("QUIT", b.handleJoinPart) | ||||
| 	i.Handlers.Add("KICK", b.handleJoinPart) | ||||
| 	i.Handlers.AddBg(girc.NOTICE, b.handleNotice) | ||||
| 	i.Handlers.AddBg("JOIN", b.handleJoinPart) | ||||
| 	i.Handlers.AddBg("PART", b.handleJoinPart) | ||||
| 	i.Handlers.AddBg("QUIT", b.handleJoinPart) | ||||
| 	i.Handlers.AddBg("KICK", b.handleJoinPart) | ||||
| 	i.Handlers.Add("INVITE", b.handleInvite) | ||||
| } | ||||
|  | ||||
| func (b *Birc) handleNickServ() { | ||||
| @@ -170,14 +194,30 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { | ||||
| 	if b.skipPrivMsg(event) { | ||||
| 		return | ||||
| 	} | ||||
| 	rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host} | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		Username: event.Source.Name, | ||||
| 		Channel:  strings.ToLower(event.Params[0]), | ||||
| 		Account:  b.Account, | ||||
| 		UserID:   event.Source.Ident + "@" + event.Source.Host, | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event) | ||||
|  | ||||
| 	// set action event | ||||
| 	if event.IsAction() { | ||||
| 	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 | ||||
| 	} | ||||
|  | ||||
| 	// set NOTICE event | ||||
| 	if event.Command == "NOTICE" { | ||||
| 		rmsg.Event = config.EventNoticeIRC | ||||
| 	} | ||||
|  | ||||
| 	// strip action, we made an event if it was an action | ||||
| 	rmsg.Text += event.StripAction() | ||||
|  | ||||
| @@ -200,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 { | ||||
| @@ -217,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,8 +2,10 @@ package birc | ||||
|  | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"hash/crc32" | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| @@ -14,6 +16,7 @@ import ( | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/lrstanley/girc" | ||||
| 	stripmd "github.com/writeas/go-strip-markdown" | ||||
|  | ||||
| 	// We need to import the 'data' package as an implicit dependency. | ||||
| 	// See: https://godoc.org/github.com/paulrosania/go-charset/charset | ||||
| @@ -28,6 +31,7 @@ type Birc struct { | ||||
| 	Local                                     chan config.Message // local queue for flood control | ||||
| 	FirstConnection, authDone                 bool | ||||
| 	MessageDelay, MessageQueue, MessageLength int | ||||
| 	channels                                  map[string]bool | ||||
|  | ||||
| 	*bridge.Config | ||||
| } | ||||
| @@ -38,6 +42,8 @@ func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	b.Nick = b.GetString("Nick") | ||||
| 	b.names = make(map[string][]string) | ||||
| 	b.connected = make(chan error) | ||||
| 	b.channels = make(map[string]bool) | ||||
|  | ||||
| 	if b.GetInt("MessageDelay") == 0 { | ||||
| 		b.MessageDelay = 1300 | ||||
| 	} else { | ||||
| @@ -67,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")) | ||||
|  | ||||
| @@ -110,6 +120,7 @@ func (b *Birc) Disconnect() error { | ||||
| } | ||||
|  | ||||
| func (b *Birc) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	b.channels[channel.Name] = true | ||||
| 	// need to check if we have nickserv auth done before joining channels | ||||
| 	for { | ||||
| 		if b.authDone { | ||||
| @@ -156,10 +167,14 @@ func (b *Birc) Send(msg config.Message) (string, error) { | ||||
| 	} | ||||
|  | ||||
| 	var msgLines []string | ||||
| 	if b.GetBool("StripMarkdown") { | ||||
| 		msg.Text = stripmd.Strip(msg.Text) | ||||
| 	} | ||||
|  | ||||
| 	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 { | ||||
| @@ -195,22 +210,58 @@ func (b *Birc) doConnect() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Sanitize nicks for RELAYMSG: replace IRC characters with special meanings with "-" | ||||
| func sanitizeNick(nick string) string { | ||||
| 	sanitize := func(r rune) rune { | ||||
| 		if strings.ContainsRune("!+%@&#$:'\"?*,. ", r) { | ||||
| 			return '-' | ||||
| 		} | ||||
| 		return r | ||||
| 	} | ||||
| 	return strings.Map(sanitize, nick) | ||||
| } | ||||
|  | ||||
| func (b *Birc) doSend() { | ||||
| 	rate := time.Millisecond * time.Duration(b.MessageDelay) | ||||
| 	throttle := time.NewTicker(rate) | ||||
| 	for msg := range b.Local { | ||||
| 		<-throttle.C | ||||
| 		username := msg.Username | ||||
| 		if b.GetBool("Colornicks") { | ||||
| 			checksum := crc32.ChecksumIEEE([]byte(msg.Username)) | ||||
| 			colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes | ||||
| 			username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) | ||||
| 		} | ||||
| 		if msg.Event == config.EventUserAction { | ||||
| 			b.i.Cmd.Action(msg.Channel, username+msg.Text) | ||||
| 		// Optional support for the proposed RELAYMSG extension, described at | ||||
| 		// https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md | ||||
| 		// nolint:nestif | ||||
| 		if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) && | ||||
| 			b.GetBool("UseRelayMsg") { | ||||
| 			username = sanitizeNick(username) | ||||
| 			text := msg.Text | ||||
|  | ||||
| 			// Work around girc chomping leading commas on single word messages? | ||||
| 			if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') { | ||||
| 				text = ":" + text | ||||
| 			} | ||||
|  | ||||
| 			if msg.Event == config.EventUserAction { | ||||
| 				b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck | ||||
| 			} else { | ||||
| 				b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username) | ||||
| 				b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck | ||||
| 			} | ||||
| 		} else { | ||||
| 			b.Log.Debugf("Sending to channel %s", msg.Channel) | ||||
| 			b.i.Cmd.Message(msg.Channel, username+msg.Text) | ||||
| 			if b.GetBool("Colornicks") { | ||||
| 				checksum := crc32.ChecksumIEEE([]byte(msg.Username)) | ||||
| 				colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes | ||||
| 				username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) | ||||
| 			} | ||||
| 			switch msg.Event { | ||||
| 			case config.EventUserAction: | ||||
| 				b.i.Cmd.Action(msg.Channel, username+msg.Text) | ||||
| 			case config.EventNoticeIRC: | ||||
| 				b.Log.Debugf("Sending notice to channel %s", msg.Channel) | ||||
| 				b.i.Cmd.Notice(msg.Channel, username+msg.Text) | ||||
| 			default: | ||||
| 				b.Log.Debugf("Sending to channel %s", msg.Channel) | ||||
| 				b.i.Cmd.Message(msg.Channel, username+msg.Text) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -225,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" | ||||
| @@ -234,6 +288,27 @@ 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 { | ||||
| 		debug = b.Log.Writer() | ||||
| 	} | ||||
|  | ||||
| 	pingDelay, err := time.ParseDuration(b.GetString("pingdelay")) | ||||
| 	if err != nil || pingDelay == 0 { | ||||
| 		pingDelay = time.Minute | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("setting pingdelay to %s", pingDelay) | ||||
|  | ||||
| 	tlsConfig, err := b.getTLSConfig() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	i := girc.New(girc.Config{ | ||||
| 		Server:     server, | ||||
| @@ -241,10 +316,15 @@ func (b *Birc) getClient() (*girc.Client, error) { | ||||
| 		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 | ||||
| 		PingDelay:  time.Minute, | ||||
| 		Bind:       b.GetString("Bind"), | ||||
| 		TLSConfig:  tlsConfig, | ||||
| 		PingDelay:  pingDelay, | ||||
| 		// skip gIRC internal rate limiting, since we have our own throttling | ||||
| 		AllowFlood:    true, | ||||
| 		Debug:         debug, | ||||
| 		SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil}, | ||||
| 	}) | ||||
| 	return i, nil | ||||
| } | ||||
| @@ -254,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) | ||||
| @@ -270,7 +354,7 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool { | ||||
| 	b.Nick = b.i.GetNick() | ||||
|  | ||||
| 	// freenode doesn't send 001 as first reply | ||||
| 	if event.Command == "NOTICE" { | ||||
| 	if event.Command == "NOTICE" && len(event.Params) != 2 { | ||||
| 		return true | ||||
| 	} | ||||
| 	// don't forward queries to the bot | ||||
| @@ -278,7 +362,18 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool { | ||||
| 		return true | ||||
| 	} | ||||
| 	// don't forward message from ourself | ||||
| 	if event.Source.Name == b.Nick { | ||||
| 	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 { | ||||
| 		return true | ||||
| 	} | ||||
| 	// This is the old name of the cap sent in spoofed messages; I've kept this in | ||||
| 	// for compatibility reasons | ||||
| 	if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| @@ -298,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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										215
									
								
								bridge/matrix/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,215 @@ | ||||
| package bmatrix | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"html" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	matrix "github.com/matterbridge/gomatrix" | ||||
| ) | ||||
|  | ||||
| func newMatrixUsername(username string) *matrixUsername { | ||||
| 	mUsername := new(matrixUsername) | ||||
|  | ||||
| 	// check if we have a </tag>. if we have, we don't escape HTML. #696 | ||||
| 	if htmlTag.MatchString(username) { | ||||
| 		mUsername.formatted = username | ||||
| 		// remove the HTML formatting for beautiful push messages #1188 | ||||
| 		mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "") | ||||
| 	} else { | ||||
| 		mUsername.formatted = html.EscapeString(username) | ||||
| 		mUsername.plain = username | ||||
| 	} | ||||
|  | ||||
| 	return mUsername | ||||
| } | ||||
|  | ||||
| // getRoomID retrieves a matching room ID from the channel name. | ||||
| func (b *Bmatrix) getRoomID(channel string) string { | ||||
| 	b.RLock() | ||||
| 	defer b.RUnlock() | ||||
| 	for ID, name := range b.RoomMap { | ||||
| 		if name == channel { | ||||
| 			return ID | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| // interface2Struct marshals and immediately unmarshals an interface. | ||||
| // Useful for converting map[string]interface{} to a struct. | ||||
| func interface2Struct(in interface{}, out interface{}) error { | ||||
| 	jsonObj, err := json.Marshal(in) | ||||
| 	if err != nil { | ||||
| 		return err //nolint:wrapcheck | ||||
| 	} | ||||
|  | ||||
| 	return json.Unmarshal(jsonObj, out) | ||||
| } | ||||
|  | ||||
| // getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache. | ||||
| func (b *Bmatrix) getDisplayName(mxid string) string { | ||||
| 	if b.GetBool("UseUserName") { | ||||
| 		return mxid[1:] | ||||
| 	} | ||||
|  | ||||
| 	b.RLock() | ||||
| 	if val, present := b.NicknameMap[mxid]; present { | ||||
| 		b.RUnlock() | ||||
|  | ||||
| 		return val.displayName | ||||
| 	} | ||||
| 	b.RUnlock() | ||||
|  | ||||
| 	displayName, err := b.mc.GetDisplayName(mxid) | ||||
| 	var httpError *matrix.HTTPError | ||||
| 	if errors.As(err, &httpError) { | ||||
| 		b.Log.Warnf("Couldn't retrieve the display name for %s", mxid) | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return b.cacheDisplayName(mxid, mxid[1:]) | ||||
| 	} | ||||
|  | ||||
| 	return b.cacheDisplayName(mxid, displayName.DisplayName) | ||||
| } | ||||
|  | ||||
| // cacheDisplayName stores the mapping between a mxid and a display name, to be reused later without performing a query to the homserver. | ||||
| // Note that old entries are cleaned when this function is called. | ||||
| func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string { | ||||
| 	now := time.Now() | ||||
|  | ||||
| 	// scan to delete old entries, to stop memory usage from becoming too high with old entries. | ||||
| 	// In addition, we also detect if another user have the same username, and if so, we append their mxids to their usernames to differentiate them. | ||||
| 	toDelete := []string{} | ||||
| 	conflict := false | ||||
|  | ||||
| 	b.Lock() | ||||
| 	for mxid, v := range b.NicknameMap { | ||||
| 		// to prevent username reuse across matrix servers - or even on the same server, append | ||||
| 		// the mxid to the username when there is a conflict | ||||
| 		if v.displayName == displayName { | ||||
| 			conflict = true | ||||
| 			// TODO: it would be nice to be able to rename previous messages from this user. | ||||
| 			// The current behavior is that only users with clashing usernames and *that have spoken since the bridge last started* will get their mxids shown, and I don't know if that's the expected behavior. | ||||
| 			v.displayName = fmt.Sprintf("%s (%s)", displayName, mxid) | ||||
| 			b.NicknameMap[mxid] = v | ||||
| 		} | ||||
|  | ||||
| 		if now.Sub(v.lastUpdated) > 10*time.Minute { | ||||
| 			toDelete = append(toDelete, mxid) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if conflict { | ||||
| 		displayName = fmt.Sprintf("%s (%s)", displayName, mxid) | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range toDelete { | ||||
| 		delete(b.NicknameMap, v) | ||||
| 	} | ||||
|  | ||||
| 	b.NicknameMap[mxid] = NicknameCacheEntry{ | ||||
| 		displayName: displayName, | ||||
| 		lastUpdated: now, | ||||
| 	} | ||||
| 	b.Unlock() | ||||
|  | ||||
| 	return displayName | ||||
| } | ||||
|  | ||||
| // handleError converts errors into httpError. | ||||
| //nolint:exhaustivestruct | ||||
| func handleError(err error) *httpError { | ||||
| 	var mErr matrix.HTTPError | ||||
| 	if !errors.As(err, &mErr) { | ||||
| 		return &httpError{ | ||||
| 			Err: "not a HTTPError", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var httpErr httpError | ||||
|  | ||||
| 	if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil { | ||||
| 		return &httpError{ | ||||
| 			Err: "unmarshal failed", | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return &httpErr | ||||
| } | ||||
|  | ||||
| func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool { | ||||
| 	// Skip empty messages | ||||
| 	if content["msgtype"] == nil { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	// Only allow image,video or file msgtypes | ||||
| 	if !(content["msgtype"].(string) == "m.image" || | ||||
| 		content["msgtype"].(string) == "m.video" || | ||||
| 		content["msgtype"].(string) == "m.file") { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // getAvatarURL returns the avatar URL of the specified sender. | ||||
| func (b *Bmatrix) getAvatarURL(sender string) string { | ||||
| 	urlPath := b.mc.BuildURL("profile", sender, "avatar_url") | ||||
|  | ||||
| 	s := struct { | ||||
| 		AvatarURL string `json:"avatar_url"` | ||||
| 	}{} | ||||
|  | ||||
| 	err := b.mc.MakeRequest("GET", urlPath, nil, &s) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("getAvatarURL failed: %s", err) | ||||
|  | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/") | ||||
| 	if url != "" { | ||||
| 		url += "?width=37&height=37&method=crop" | ||||
| 	} | ||||
|  | ||||
| 	return url | ||||
| } | ||||
|  | ||||
| // handleRatelimit handles the ratelimit errors and return if we're ratelimited and the amount of time to sleep | ||||
| func (b *Bmatrix) handleRatelimit(err error) (time.Duration, bool) { | ||||
| 	httpErr := handleError(err) | ||||
| 	if httpErr.Errcode != "M_LIMIT_EXCEEDED" { | ||||
| 		return 0, false | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("ratelimited: %s", httpErr.Err) | ||||
| 	b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before retrying", httpErr.RetryAfterMs/1000) | ||||
|  | ||||
| 	return time.Duration(httpErr.RetryAfterMs) * time.Millisecond, true | ||||
| } | ||||
|  | ||||
| // retry function will check if we're ratelimited and retries again when backoff time expired | ||||
| // returns original error if not 429 ratelimit | ||||
| func (b *Bmatrix) retry(f func() error) error { | ||||
| 	b.rateMutex.Lock() | ||||
| 	defer b.rateMutex.Unlock() | ||||
|  | ||||
| 	for { | ||||
| 		if err := f(); err != nil { | ||||
| 			if backoff, ok := b.handleRatelimit(err); ok { | ||||
| 				time.Sleep(backoff) | ||||
| 			} else { | ||||
| 				return err | ||||
| 			} | ||||
| 		} else { | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -3,11 +3,11 @@ package bmatrix | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"html" | ||||
| 	"mime" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| @@ -15,40 +15,108 @@ import ( | ||||
| 	matrix "github.com/matterbridge/gomatrix" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	htmlTag            = regexp.MustCompile("</.*?>") | ||||
| 	htmlReplacementTag = regexp.MustCompile("<[^>]*>") | ||||
| ) | ||||
|  | ||||
| type NicknameCacheEntry struct { | ||||
| 	displayName string | ||||
| 	lastUpdated time.Time | ||||
| } | ||||
|  | ||||
| type Bmatrix struct { | ||||
| 	mc      *matrix.Client | ||||
| 	UserID  string | ||||
| 	RoomMap map[string]string | ||||
| 	mc          *matrix.Client | ||||
| 	UserID      string | ||||
| 	NicknameMap map[string]NicknameCacheEntry | ||||
| 	RoomMap     map[string]string | ||||
| 	rateMutex   sync.RWMutex | ||||
| 	sync.RWMutex | ||||
| 	htmlTag *regexp.Regexp | ||||
| 	*bridge.Config | ||||
| } | ||||
|  | ||||
| type httpError struct { | ||||
| 	Errcode      string `json:"errcode"` | ||||
| 	Err          string `json:"error"` | ||||
| 	RetryAfterMs int    `json:"retry_after_ms"` | ||||
| } | ||||
|  | ||||
| type matrixUsername struct { | ||||
| 	plain     string | ||||
| 	formatted string | ||||
| } | ||||
|  | ||||
| // SubTextMessage represents the new content of the message in edit messages. | ||||
| type SubTextMessage struct { | ||||
| 	MsgType       string `json:"msgtype"` | ||||
| 	Body          string `json:"body"` | ||||
| 	FormattedBody string `json:"formatted_body,omitempty"` | ||||
| 	Format        string `json:"format,omitempty"` | ||||
| } | ||||
|  | ||||
| // MessageRelation explains how the current message relates to a previous message. | ||||
| // Notably used for message edits. | ||||
| type MessageRelation struct { | ||||
| 	EventID string `json:"event_id"` | ||||
| 	Type    string `json:"rel_type"` | ||||
| } | ||||
|  | ||||
| type EditedMessage struct { | ||||
| 	NewContent SubTextMessage  `json:"m.new_content"` | ||||
| 	RelatedTo  MessageRelation `json:"m.relates_to"` | ||||
| 	matrix.TextMessage | ||||
| } | ||||
|  | ||||
| type InReplyToRelationContent struct { | ||||
| 	EventID string `json:"event_id"` | ||||
| } | ||||
|  | ||||
| type InReplyToRelation struct { | ||||
| 	InReplyTo InReplyToRelationContent `json:"m.in_reply_to"` | ||||
| } | ||||
|  | ||||
| type ReplyMessage struct { | ||||
| 	RelatedTo InReplyToRelation `json:"m.relates_to"` | ||||
| 	matrix.TextMessage | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	b := &Bmatrix{Config: cfg} | ||||
| 	b.htmlTag = regexp.MustCompile("</.*?>") | ||||
| 	b.RoomMap = make(map[string]string) | ||||
| 	b.NicknameMap = make(map[string]NicknameCacheEntry) | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| 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"), | ||||
| 	}) | ||||
| 	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 | ||||
| } | ||||
| @@ -58,14 +126,18 @@ func (b *Bmatrix) Disconnect() error { | ||||
| } | ||||
|  | ||||
| func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	resp, err := b.mc.JoinRoom(channel.Name, "", nil) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	b.Lock() | ||||
| 	b.RoomMap[resp.RoomID] = channel.Name | ||||
| 	b.Unlock() | ||||
| 	return err | ||||
| 	return b.retry(func() error { | ||||
| 		resp, err := b.mc.JoinRoom(channel.Name, "", nil) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		b.Lock() | ||||
| 		b.RoomMap[resp.RoomID] = channel.Name | ||||
| 		b.Unlock() | ||||
|  | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||
| @@ -74,17 +146,61 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||
| 	channel := b.getRoomID(msg.Channel) | ||||
| 	b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel) | ||||
|  | ||||
| 	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:    msg.Username + msg.Text, | ||||
| 			MsgType:       "m.emote", | ||||
| 			Body:          body, | ||||
| 			FormattedBody: formattedBody, | ||||
| 			Format:        "org.matrix.custom.html", | ||||
| 		} | ||||
| 		resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
|  | ||||
| 		if b.GetBool("HTMLDisable") { | ||||
| 			m.Format = "" | ||||
| 			m.FormattedBody = "" | ||||
| 		} | ||||
| 		return resp.EventID, err | ||||
|  | ||||
| 		msgID := "" | ||||
|  | ||||
| 		err := b.retry(func() error { | ||||
| 			resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			msgID = resp.EventID | ||||
|  | ||||
| 			return err | ||||
| 		}) | ||||
|  | ||||
| 		return msgID, err | ||||
| 	} | ||||
|  | ||||
| 	// Delete message | ||||
| @@ -92,17 +208,34 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||
| 		if msg.ID == "" { | ||||
| 			return "", nil | ||||
| 		} | ||||
| 		resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{}) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		return resp.EventID, err | ||||
|  | ||||
| 		msgID := "" | ||||
|  | ||||
| 		err := b.retry(func() error { | ||||
| 			resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{}) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			msgID = resp.EventID | ||||
|  | ||||
| 			return err | ||||
| 		}) | ||||
|  | ||||
| 		return msgID, err | ||||
| 	} | ||||
|  | ||||
| 	// Upload a file if it exists | ||||
| 	if msg.Extra != nil { | ||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | ||||
| 			if _, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text); err != nil { | ||||
| 			rmsg := rmsg | ||||
|  | ||||
| 			err := b.retry(func() error { | ||||
| 				_, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text) | ||||
|  | ||||
| 				return err | ||||
| 			}) | ||||
| 			if err != nil { | ||||
| 				b.Log.Errorf("sendText failed: %s", err) | ||||
| 			} | ||||
| 		} | ||||
| @@ -113,47 +246,162 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||
| 	} | ||||
|  | ||||
| 	// Edit message if we have an ID | ||||
| 	// matrix has no editing support | ||||
| 	if msg.ID != "" { | ||||
| 		rmsg := EditedMessage{ | ||||
| 			TextMessage: matrix.TextMessage{ | ||||
| 				Body:          body, | ||||
| 				MsgType:       "m.text", | ||||
| 				Format:        "org.matrix.custom.html", | ||||
| 				FormattedBody: formattedBody, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 	// Use notices to send join/leave events | ||||
| 	if msg.Event == config.EventJoinLeave { | ||||
| 		resp, err := b.mc.SendNotice(channel, msg.Username+msg.Text) | ||||
| 		rmsg.NewContent = SubTextMessage{ | ||||
| 			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", | ||||
| 		} | ||||
|  | ||||
| 		err := b.retry(func() error { | ||||
| 			_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg) | ||||
|  | ||||
| 			return err | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		return msg.ID, nil | ||||
| 	} | ||||
|  | ||||
| 	// Use notices to send join/leave events | ||||
| 	if msg.Event == config.EventJoinLeave { | ||||
| 		m := matrix.TextMessage{ | ||||
| 			MsgType:       "m.notice", | ||||
| 			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 | ||||
| 	} | ||||
|  | ||||
| 	username := html.EscapeString(msg.Username) | ||||
| 	// check if we have a </tag>. if we have, we don't escape HTML. #696 | ||||
| 	if b.htmlTag.MatchString(msg.Username) { | ||||
| 		username = msg.Username | ||||
| 	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 ( | ||||
| 			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 b.GetBool("HTMLDisable") { | ||||
| 		var ( | ||||
| 			resp *matrix.RespSendEvent | ||||
| 			err  error | ||||
| 		) | ||||
|  | ||||
| 		err = b.retry(func() error { | ||||
| 			resp, err = b.mc.SendText(channel, body) | ||||
|  | ||||
| 			return err | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		return resp.EventID, err | ||||
| 	} | ||||
|  | ||||
| 	// Post normal message with HTML support (eg riot.im) | ||||
| 	resp, err := b.mc.SendHTML(channel, msg.Username+msg.Text, username+helper.ParseMarkdown(msg.Text)) | ||||
| 	var ( | ||||
| 		resp *matrix.RespSendEvent | ||||
| 		err  error | ||||
| 	) | ||||
|  | ||||
| 	err = b.retry(func() error { | ||||
| 		resp, err = b.mc.SendFormattedText(channel, body, formattedBody) | ||||
|  | ||||
| 		return err | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return resp.EventID, err | ||||
| } | ||||
|  | ||||
| func (b *Bmatrix) getRoomID(channel string) string { | ||||
| 	b.RLock() | ||||
| 	defer b.RUnlock() | ||||
| 	for ID, name := range b.RoomMap { | ||||
| 		if name == channel { | ||||
| 			return ID | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| 	return resp.EventID, err | ||||
| } | ||||
|  | ||||
| func (b *Bmatrix) handlematrix() { | ||||
| 	syncer := b.mc.Syncer.(*matrix.DefaultSyncer) | ||||
| 	syncer.OnEventType("m.room.redaction", b.handleEvent) | ||||
| 	syncer.OnEventType("m.room.message", b.handleEvent) | ||||
| 	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) | ||||
| 			} | ||||
| @@ -161,6 +409,77 @@ func (b *Bmatrix) handlematrix() { | ||||
| 	}() | ||||
| } | ||||
|  | ||||
| func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool { | ||||
| 	relationInterface, present := ev.Content["m.relates_to"] | ||||
| 	newContentInterface, present2 := ev.Content["m.new_content"] | ||||
| 	if !(present && present2) { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	var relation MessageRelation | ||||
| 	if err := interface2Struct(relationInterface, &relation); err != nil { | ||||
| 		b.Log.Warnf("Couldn't parse 'm.relates_to' object with value %#v", relationInterface) | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	var newContent SubTextMessage | ||||
| 	if err := interface2Struct(newContentInterface, &newContent); err != nil { | ||||
| 		b.Log.Warnf("Couldn't parse 'm.new_content' object with value %#v", newContentInterface) | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	if relation.Type != "m.replace" { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	rmsg.ID = relation.EventID | ||||
| 	rmsg.Text = newContent.Body | ||||
| 	b.Remote <- rmsg | ||||
|  | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool { | ||||
| 	relationInterface, present := ev.Content["m.relates_to"] | ||||
| 	if !present { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	var relation InReplyToRelation | ||||
| 	if err := interface2Struct(relationInterface, &relation); err != nil { | ||||
| 		// probably fine | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	body := rmsg.Text | ||||
|  | ||||
| 	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" { | ||||
| 		if dn, ok := ev.Content["displayname"].(string); ok { | ||||
| 			b.cacheDisplayName(ev.Sender, dn) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bmatrix) handleEvent(ev *matrix.Event) { | ||||
| 	b.Log.Debugf("== Receiving event: %#v", ev) | ||||
| 	if ev.Sender != b.UserID { | ||||
| @@ -174,7 +493,7 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { | ||||
|  | ||||
| 		// Create our message | ||||
| 		rmsg := config.Message{ | ||||
| 			Username: ev.Sender[1:], | ||||
| 			Username: b.getDisplayName(ev.Sender), | ||||
| 			Channel:  channel, | ||||
| 			Account:  b.Account, | ||||
| 			UserID:   ev.Sender, | ||||
| @@ -182,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("(.*?):.*") | ||||
| @@ -204,11 +516,28 @@ 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 | ||||
| 		} | ||||
|  | ||||
| 		// Is it an edit? | ||||
| 		if b.handleEdit(ev, rmsg) { | ||||
| 			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) | ||||
| @@ -219,6 +548,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { | ||||
|  | ||||
| 		b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account) | ||||
| 		b.Remote <- rmsg | ||||
|  | ||||
| 		// not crucial, so no ratelimit check here | ||||
| 		if err := b.mc.MarkRead(ev.RoomID, ev.ID); err != nil { | ||||
| 			b.Log.Errorf("couldn't mark message as read %s", err.Error()) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -293,27 +627,30 @@ func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string | ||||
|  | ||||
| // handleUploadFile handles native upload of a file. | ||||
| func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) { | ||||
| 	username := newMatrixUsername(msg.Username) | ||||
| 	content := bytes.NewReader(*fi.Data) | ||||
| 	sp := strings.Split(fi.Name, ".") | ||||
| 	mtype := mime.TypeByExtension("." + sp[len(sp)-1]) | ||||
| 	if !(strings.Contains(mtype, "image") || strings.Contains(mtype, "video") || | ||||
| 		strings.Contains(mtype, "application") || strings.Contains(mtype, "audio")) { | ||||
| 		return | ||||
| 	} | ||||
| 	if fi.Comment != "" { | ||||
| 		_, err := b.mc.SendText(channel, msg.Username+fi.Comment) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("file comment failed: %#v", err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		// image and video uploads send no username, we have to do this ourself here #715 | ||||
| 		_, err := b.mc.SendText(channel, msg.Username) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("file comment failed: %#v", err) | ||||
| 		} | ||||
| 	// image and video uploads send no username, we have to do this ourself here #715 | ||||
| 	err := b.retry(func() error { | ||||
| 		_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment) | ||||
|  | ||||
| 		return err | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("file comment failed: %#v", err) | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("uploading file: %s %s", fi.Name, mtype) | ||||
| 	res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data))) | ||||
|  | ||||
| 	var res *matrix.RespMediaUpload | ||||
|  | ||||
| 	err = b.retry(func() error { | ||||
| 		res, err = b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data))) | ||||
|  | ||||
| 		return err | ||||
| 	}) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("file upload failed: %#v", err) | ||||
| 		return | ||||
| @@ -322,56 +659,60 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf | ||||
| 	switch { | ||||
| 	case strings.Contains(mtype, "video"): | ||||
| 		b.Log.Debugf("sendVideo %s", res.ContentURI) | ||||
| 		_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI) | ||||
| 		err = b.retry(func() error { | ||||
| 			_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI) | ||||
|  | ||||
| 			return err | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("sendVideo failed: %#v", err) | ||||
| 		} | ||||
| 	case strings.Contains(mtype, "image"): | ||||
| 		b.Log.Debugf("sendImage %s", res.ContentURI) | ||||
| 		_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI) | ||||
| 		err = b.retry(func() error { | ||||
| 			_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI) | ||||
|  | ||||
| 			return err | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("sendImage failed: %#v", err) | ||||
| 		} | ||||
| 	case strings.Contains(mtype, "application"): | ||||
| 		b.Log.Debugf("sendFile %s", res.ContentURI) | ||||
| 		_, err = b.mc.SendFile(channel, fi.Name, res.ContentURI, mtype, uint(len(*fi.Data))) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("sendFile failed: %#v", err) | ||||
| 		} | ||||
| 	case strings.Contains(mtype, "audio"): | ||||
| 		b.Log.Debugf("sendAudio %s", res.ContentURI) | ||||
| 		_, err = b.mc.SendAudio(channel, fi.Name, res.ContentURI, mtype, uint(len(*fi.Data))) | ||||
| 		err = b.retry(func() error { | ||||
| 			_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{ | ||||
| 				MsgType: "m.audio", | ||||
| 				Body:    fi.Name, | ||||
| 				URL:     res.ContentURI, | ||||
| 				Info: matrix.AudioInfo{ | ||||
| 					Mimetype: mtype, | ||||
| 					Size:     uint(len(*fi.Data)), | ||||
| 				}, | ||||
| 			}) | ||||
|  | ||||
| 			return err | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("sendAudio failed: %#v", err) | ||||
| 		} | ||||
| 	default: | ||||
| 		b.Log.Debugf("sendFile %s", res.ContentURI) | ||||
| 		err = b.retry(func() error { | ||||
| 			_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{ | ||||
| 				MsgType: "m.file", | ||||
| 				Body:    fi.Name, | ||||
| 				URL:     res.ContentURI, | ||||
| 				Info: matrix.FileInfo{ | ||||
| 					Mimetype: mtype, | ||||
| 					Size:     uint(len(*fi.Data)), | ||||
| 				}, | ||||
| 			}) | ||||
|  | ||||
| 			return err | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("sendFile failed: %#v", err) | ||||
| 		} | ||||
| 	} | ||||
| 	b.Log.Debugf("result: %#v", res) | ||||
| } | ||||
|  | ||||
| // skipMessages returns true if this message should not be handled | ||||
| func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool { | ||||
| 	// Skip empty messages | ||||
| 	if content["msgtype"] == nil { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	// Only allow image,video or file msgtypes | ||||
| 	if !(content["msgtype"].(string) == "m.image" || | ||||
| 		content["msgtype"].(string) == "m.video" || | ||||
| 		content["msgtype"].(string) == "m.file") { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // getAvatarURL returns the avatar URL of the specified sender | ||||
| func (b *Bmatrix) getAvatarURL(sender string) string { | ||||
| 	mxcURL, err := b.mc.GetSenderAvatarURL(sender) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("getAvatarURL failed: %s", err) | ||||
| 		return "" | ||||
| 	} | ||||
| 	url := strings.ReplaceAll(mxcURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/") | ||||
| 	url += "?width=37&height=37&method=crop" | ||||
| 	return url | ||||
| } | ||||
|   | ||||
							
								
								
									
										28
									
								
								bridge/matrix/matrix_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | ||||
| package bmatrix | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestPlainUsername(t *testing.T) { | ||||
| 	uut := newMatrixUsername("MyUser") | ||||
|  | ||||
| 	assert.Equal(t, "MyUser", uut.formatted) | ||||
| 	assert.Equal(t, "MyUser", uut.plain) | ||||
| } | ||||
|  | ||||
| func TestHTMLUsername(t *testing.T) { | ||||
| 	uut := newMatrixUsername("<b>MyUser</b>") | ||||
|  | ||||
| 	assert.Equal(t, "<b>MyUser</b>", uut.formatted) | ||||
| 	assert.Equal(t, "MyUser", uut.plain) | ||||
| } | ||||
|  | ||||
| func TestFancyUsername(t *testing.T) { | ||||
| 	uut := newMatrixUsername("<MyUser>") | ||||
|  | ||||
| 	assert.Equal(t, "<MyUser>", uut.formatted) | ||||
| 	assert.Equal(t, "<MyUser>", uut.plain) | ||||
| } | ||||
| @@ -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/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,10 +118,10 @@ 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.ParentId, | ||||
| 			ParentID: message.Post.RootId, // ParentID is obsolete with mattermost | ||||
| 			Extra:    make(map[string][]interface{}), | ||||
| 		} | ||||
|  | ||||
| @@ -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/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,19 +146,31 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) { | ||||
| 		if msg.ID == "" { | ||||
| 			return "", nil | ||||
| 		} | ||||
|  | ||||
| 		return msg.ID, b.mc.DeleteMessage(msg.ID) | ||||
| 	} | ||||
|  | ||||
| 	// Handle prefix hint for unthreaded messages. | ||||
| 	if msg.ParentID == "msg-parent-not-found" { | ||||
| 	if msg.ParentNotFound() { | ||||
| 		msg.ParentID = "" | ||||
| 		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) | ||||
| 	} | ||||
|  | ||||
| 	// we only can reply to the root of the thread, not to a specific ID (like discord for example does) | ||||
| 	if msg.ParentID != "" { | ||||
| 		post, _, 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 | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 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) | ||||
| 			} | ||||
| 		} | ||||
| @@ -150,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) | ||||
| } | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import ( | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/davecgh/go-spew/spew" | ||||
|  | ||||
| 	"github.com/mattn/godown" | ||||
| 	msgraph "github.com/yaegashi/msgraph.go/beta" | ||||
| @@ -18,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 | ||||
| @@ -49,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) | ||||
| 	} | ||||
| @@ -71,19 +74,30 @@ func (b *Bmsteams) Disconnect() error { | ||||
| } | ||||
|  | ||||
| func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	go b.poll(channel.Name) | ||||
| 	go func(name string) { | ||||
| 		for { | ||||
| 			err := b.poll(name) | ||||
| 			if err != nil { | ||||
| 				b.Log.Errorf("polling failed for %s: %s. retrying in 5 seconds", name, err) | ||||
| 			} | ||||
| 			time.Sleep(time.Second * 5) | ||||
| 		} | ||||
| 	}(channel.Name) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bmsteams) Send(msg config.Message) (string, error) { | ||||
| 	b.Log.Debugf("=> Receiving %#v", msg) | ||||
| 	if msg.ParentID != "" && msg.ParentID != "msg-parent-not-found" { | ||||
| 	if msg.ParentValid() { | ||||
| 		return b.sendReply(msg) | ||||
| 	} | ||||
| 	if msg.ParentID == "msg-parent-not-found" { | ||||
|  | ||||
| 	// Handle prefix hint for unthreaded messages. | ||||
| 	if msg.ParentNotFound() { | ||||
| 		msg.ParentID = "" | ||||
| 		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) | ||||
| 	} | ||||
|  | ||||
| 	ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request() | ||||
| 	text := msg.Username + msg.Text | ||||
| 	content := &msgraph.ItemBody{Content: &text} | ||||
| @@ -120,12 +134,12 @@ func (b *Bmsteams) getMessages(channel string) ([]msgraph.ChatMessage, error) { | ||||
| } | ||||
|  | ||||
| //nolint:gocognit | ||||
| func (b *Bmsteams) poll(channelName string) { | ||||
| func (b *Bmsteams) poll(channelName string) error { | ||||
| 	msgmap := make(map[string]time.Time) | ||||
| 	b.Log.Debug("getting initial messages") | ||||
| 	res, err := b.getMessages(channelName) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, msg := range res { | ||||
| 		msgmap[*msg.ID] = *msg.CreatedDateTime | ||||
| @@ -138,7 +152,7 @@ func (b *Bmsteams) poll(channelName string) { | ||||
| 	for { | ||||
| 		res, err := b.getMessages(channelName) | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 			return err | ||||
| 		} | ||||
| 		for i := len(res) - 1; i >= 0; i-- { | ||||
| 			msg := res[i] | ||||
| @@ -150,11 +164,22 @@ func (b *Bmsteams) poll(channelName string) { | ||||
| 					continue | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if b.GetBool("debug") { | ||||
| 				b.Log.Debug("Msg dump: ", spew.Sdump(msg)) | ||||
| 			} | ||||
|  | ||||
| 			// skip non-user message for now. | ||||
| 			if msg.From == nil || msg.From.User == nil { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if *msg.From.User.ID == b.botID { | ||||
| 				b.Log.Debug("skipping own message") | ||||
| 				msgmap[*msg.ID] = *msg.CreatedDateTime | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			msgmap[*msg.ID] = *msg.CreatedDateTime | ||||
| 			if msg.LastModifiedDateTime != nil { | ||||
| 				msgmap[*msg.ID] = *msg.LastModifiedDateTime | ||||
|   | ||||
							
								
								
									
										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() { | ||||
| } | ||||
							
								
								
									
										158
									
								
								bridge/mumble/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,158 @@ | ||||
| package bmumble | ||||
|  | ||||
| import ( | ||||
| 	"strconv" | ||||
| 	"time" | ||||
|  | ||||
| 	"layeh.com/gumble/gumble" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| ) | ||||
|  | ||||
| func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) { | ||||
| 	b.serverConfigUpdate <- *event | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) { | ||||
| 	sender := "unknown" | ||||
| 	if event.TextMessage.Sender != nil { | ||||
| 		sender = event.TextMessage.Sender.Name | ||||
| 	} | ||||
| 	// If the text message is received before receiving a ServerSync | ||||
| 	// and UserState, Client.Self or Self.Channel are nil | ||||
| 	if event.Client.Self == nil || event.Client.Self.Channel == nil { | ||||
| 		b.Log.Warn("Connection bootstrap not finished, discarding text message") | ||||
| 		return | ||||
| 	} | ||||
| 	// Convert Mumble HTML messages to markdown | ||||
| 	parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message) | ||||
| 	if err != nil { | ||||
| 		b.Log.Error(err) | ||||
| 	} | ||||
| 	now := time.Now().UTC() | ||||
| 	for i, part := range parts { | ||||
| 		// Construct matterbridge message and pass on to the gateway | ||||
| 		rmsg := config.Message{ | ||||
| 			Channel:  strconv.FormatUint(uint64(event.Client.Self.Channel.ID), 10), | ||||
| 			Username: sender, | ||||
| 			UserID:   sender + "@" + b.Host, | ||||
| 			Account:  b.Account, | ||||
| 		} | ||||
| 		if part.Image == nil { | ||||
| 			rmsg.Text = part.Text | ||||
| 		} else { | ||||
| 			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") | ||||
| 				continue | ||||
| 			} | ||||
| 			helper.HandleDownloadData(b.Log, &rmsg, fname, "", "", &part.Image, b.General) | ||||
| 		} | ||||
| 		b.Log.Debugf("Sending message to gateway: %+v", rmsg) | ||||
| 		b.Remote <- rmsg | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) { | ||||
| 	// Set the user's "bio"/comment | ||||
| 	if comment := b.GetString("UserComment"); comment != "" && event.Client.Self != nil { | ||||
| 		event.Client.Self.SetComment(comment) | ||||
| 	} | ||||
| 	// No need to talk or listen | ||||
| 	event.Client.Self.SetSelfDeafened(true) | ||||
| 	// if the Channel variable is set, this is a reconnect -> rejoin channel | ||||
| 	if b.Channel != nil { | ||||
| 		if err := b.doJoin(event.Client, *b.Channel); err != nil { | ||||
| 			b.Log.Error(err) | ||||
| 		} | ||||
| 		b.Remote <- config.Message{ | ||||
| 			Username: "system", | ||||
| 			Text:     "rejoin", | ||||
| 			Channel:  "", | ||||
| 			Account:  b.Account, | ||||
| 			Event:    config.EventRejoinChannels, | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) handleJoinLeave(event *gumble.UserChangeEvent) { | ||||
| 	// Ignore events happening before setup is done | ||||
| 	if b.Channel == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	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 | ||||
| } | ||||
							
								
								
									
										143
									
								
								bridge/mumble/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,143 @@ | ||||
| package bmumble | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"mime" | ||||
| 	"net/http" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/mattn/godown" | ||||
| 	"github.com/vincent-petithory/dataurl" | ||||
| ) | ||||
|  | ||||
| type MessagePart struct { | ||||
| 	Text          string | ||||
| 	FileExtension string | ||||
| 	Image         []byte | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) decodeImage(uri string, parts *[]MessagePart) error { | ||||
| 	// Decode the data:image/... URI | ||||
| 	image, err := dataurl.DecodeString(uri) | ||||
| 	if err != nil { | ||||
| 		b.Log.WithError(err).Info("No image extracted") | ||||
| 		return err | ||||
| 	} | ||||
| 	// Determine the file extensions for that image | ||||
| 	ext, err := mime.ExtensionsByType(image.MediaType.ContentType()) | ||||
| 	if err != nil || len(ext) == 0 { | ||||
| 		b.Log.WithError(err).Infof("No file extension registered for MIME type '%s'", image.MediaType.ContentType()) | ||||
| 		return err | ||||
| 	} | ||||
| 	// Add the image to the MessagePart slice | ||||
| 	*parts = append(*parts, MessagePart{"", ext[0], image.Data}) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) tokenize(t *string) ([]MessagePart, error) { | ||||
| 	// `^(.*?)` matches everything before the image | ||||
| 	// `!\[[^\]]*\]\(` matches the `]+)` matches the data: URI used by Mumble | ||||
| 	// `\)` matches the closing parenthesis after the URI | ||||
| 	// `(.*)$` matches the remaining text to be examined in the next iteration | ||||
| 	p := regexp.MustCompile(`^(?ms)(.*?)!\[[^\]]*\]\((data:image\/[^)]+)\)(.*)$`) | ||||
| 	remaining := *t | ||||
| 	var parts []MessagePart | ||||
| 	for { | ||||
| 		tokens := p.FindStringSubmatch(remaining) | ||||
| 		if tokens == nil { | ||||
| 			// no match -> remaining string is non-image text | ||||
| 			pre := strings.TrimSpace(remaining) | ||||
| 			if len(pre) > 0 { | ||||
| 				parts = append(parts, MessagePart{pre, "", nil}) | ||||
| 			} | ||||
| 			return parts, nil | ||||
| 		} | ||||
|  | ||||
| 		// tokens[1] is the text before the image | ||||
| 		if len(tokens[1]) > 0 { | ||||
| 			pre := strings.TrimSpace(tokens[1]) | ||||
| 			parts = append(parts, MessagePart{pre, "", nil}) | ||||
| 		} | ||||
| 		// tokens[2] is the image URL | ||||
| 		uri, err := dataurl.UnescapeToString(strings.TrimSpace(strings.ReplaceAll(tokens[2], " ", ""))) | ||||
| 		if err != nil { | ||||
| 			b.Log.WithError(err).Info("URL unescaping failed") | ||||
| 			remaining = strings.TrimSpace(tokens[3]) | ||||
| 			continue | ||||
| 		} | ||||
| 		err = b.decodeImage(uri, &parts) | ||||
| 		if err != nil { | ||||
| 			b.Log.WithError(err).Info("Decoding the image failed") | ||||
| 		} | ||||
| 		// tokens[3] is the text after the image, processed in the next iteration | ||||
| 		remaining = strings.TrimSpace(tokens[3]) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) convertHTMLtoMarkdown(html string) ([]MessagePart, error) { | ||||
| 	var sb strings.Builder | ||||
| 	err := godown.Convert(&sb, strings.NewReader(html), nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	markdown := sb.String() | ||||
| 	b.Log.Debugf("### to markdown: %s", markdown) | ||||
| 	return b.tokenize(&markdown) | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) extractFiles(msg *config.Message) []config.Message { | ||||
| 	var messages []config.Message | ||||
| 	if msg.Extra == nil || len(msg.Extra["file"]) == 0 { | ||||
| 		return messages | ||||
| 	} | ||||
| 	// Create a separate message for each file | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi := f.(config.FileInfo) | ||||
| 		imsg := config.Message{ | ||||
| 			Channel:   msg.Channel, | ||||
| 			Username:  msg.Username, | ||||
| 			UserID:    msg.UserID, | ||||
| 			Account:   msg.Account, | ||||
| 			Protocol:  msg.Protocol, | ||||
| 			Timestamp: msg.Timestamp, | ||||
| 			Event:     "mumble_image", | ||||
| 		} | ||||
| 		// If no data is present for the file, send a link instead | ||||
| 		if fi.Data == nil || len(*fi.Data) == 0 { | ||||
| 			if len(fi.URL) > 0 { | ||||
| 				imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL) | ||||
| 				messages = append(messages, imsg) | ||||
| 			} else { | ||||
| 				b.Log.Infof("Not forwarding file without local data") | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
| 		mimeType := http.DetectContentType(*fi.Data) | ||||
| 		// Mumble only supports images natively, send a link instead | ||||
| 		if !strings.HasPrefix(mimeType, "image/") { | ||||
| 			if len(fi.URL) > 0 { | ||||
| 				imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL) | ||||
| 				messages = append(messages, imsg) | ||||
| 			} else { | ||||
| 				b.Log.Infof("Not forwarding file of type %s", mimeType) | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
| 		mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0]) | ||||
| 		// Build data:image/...;base64,... style image URL and embed image directly into the message | ||||
| 		du := dataurl.New(*fi.Data, mimeType) | ||||
| 		dataURL, err := du.MarshalText() | ||||
| 		if err != nil { | ||||
| 			b.Log.WithError(err).Infof("Image Serialization into data URL failed (type: %s, length: %d)", mimeType, len(*fi.Data)) | ||||
| 			continue | ||||
| 		} | ||||
| 		imsg.Text = fmt.Sprintf(`<img src="%s"/>`, dataURL) | ||||
| 		messages = append(messages, imsg) | ||||
| 	} | ||||
| 	// Remove files from original message | ||||
| 	msg.Extra["file"] = nil | ||||
| 	return messages | ||||
| } | ||||
							
								
								
									
										268
									
								
								bridge/mumble/mumble.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,268 @@ | ||||
| package bmumble | ||||
|  | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"layeh.com/gumble/gumble" | ||||
| 	"layeh.com/gumble/gumbleutil" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	stripmd "github.com/writeas/go-strip-markdown" | ||||
|  | ||||
| 	// We need to import the 'data' package as an implicit dependency. | ||||
| 	// See: https://godoc.org/github.com/paulrosania/go-charset/charset | ||||
| 	_ "github.com/paulrosania/go-charset/data" | ||||
| ) | ||||
|  | ||||
| type Bmumble struct { | ||||
| 	client             *gumble.Client | ||||
| 	Nick               string | ||||
| 	Host               string | ||||
| 	Channel            *uint32 | ||||
| 	local              chan config.Message | ||||
| 	running            chan error | ||||
| 	connected          chan gumble.DisconnectEvent | ||||
| 	serverConfigUpdate chan gumble.ServerConfigEvent | ||||
| 	serverConfig       gumble.ServerConfigEvent | ||||
| 	tlsConfig          tls.Config | ||||
|  | ||||
| 	*bridge.Config | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	b := &Bmumble{} | ||||
| 	b.Config = cfg | ||||
| 	b.Nick = b.GetString("Nick") | ||||
| 	b.local = make(chan config.Message) | ||||
| 	b.running = make(chan error) | ||||
| 	b.connected = make(chan gumble.DisconnectEvent) | ||||
| 	b.serverConfigUpdate = make(chan gumble.ServerConfigEvent) | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) Connect() error { | ||||
| 	b.Log.Infof("Connecting %s", b.GetString("Server")) | ||||
| 	host, portstr, err := net.SplitHostPort(b.GetString("Server")) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	b.Host = host | ||||
| 	_, err = strconv.Atoi(portstr) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err = b.buildTLSConfig(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	go b.doSend() | ||||
| 	go b.connectLoop() | ||||
| 	err = <-b.running | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) Disconnect() error { | ||||
| 	return b.client.Disconnect() | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	cid, err := strconv.ParseUint(channel.Name, 10, 32) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	channelID := uint32(cid) | ||||
| 	if b.Channel != nil && *b.Channel != channelID { | ||||
| 		b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel) | ||||
| 		return errors.New("the Mumble bridge can only join a single channel") | ||||
| 	} | ||||
| 	b.Channel = &channelID | ||||
| 	return b.doJoin(b.client, channelID) | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) Send(msg config.Message) (string, error) { | ||||
| 	// Only process text messages | ||||
| 	b.Log.Debugf("=> Received local message %#v", msg) | ||||
| 	if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	attachments := b.extractFiles(&msg) | ||||
| 	b.local <- msg | ||||
| 	for _, a := range attachments { | ||||
| 		b.local <- a | ||||
| 	} | ||||
| 	return "", nil | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) buildTLSConfig() error { | ||||
| 	b.tlsConfig = tls.Config{} | ||||
| 	// Load TLS client certificate keypair required for registered user authentication | ||||
| 	if cpath := b.GetString("TLSClientCertificate"); cpath != "" { | ||||
| 		if ckey := b.GetString("TLSClientKey"); ckey != "" { | ||||
| 			cert, err := tls.LoadX509KeyPair(cpath, ckey) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			b.tlsConfig.Certificates = []tls.Certificate{cert} | ||||
| 		} | ||||
| 	} | ||||
| 	// Load TLS CA used for server verification.  If not provided, the Go system trust anchor is used | ||||
| 	if capath := b.GetString("TLSCACertificate"); capath != "" { | ||||
| 		ca, err := ioutil.ReadFile(capath) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		b.tlsConfig.RootCAs = x509.NewCertPool() | ||||
| 		b.tlsConfig.RootCAs.AppendCertsFromPEM(ca) | ||||
| 	} | ||||
| 	b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) connectLoop() { | ||||
| 	firstConnect := true | ||||
| 	for { | ||||
| 		err := b.doConnect() | ||||
| 		if firstConnect { | ||||
| 			b.running <- err | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("Connection to server failed: %#v", err) | ||||
| 			if firstConnect { | ||||
| 				break | ||||
| 			} else { | ||||
| 				b.Log.Info("Retrying in 10s") | ||||
| 				time.Sleep(10 * time.Second) | ||||
| 				continue | ||||
| 			} | ||||
| 		} | ||||
| 		firstConnect = false | ||||
| 		d := <-b.connected | ||||
| 		switch d.Type { | ||||
| 		case gumble.DisconnectError: | ||||
| 			b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String) | ||||
| 			continue | ||||
| 		case gumble.DisconnectKicked: | ||||
| 			b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String) | ||||
| 			continue | ||||
| 		case gumble.DisconnectBanned: | ||||
| 			b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String) | ||||
| 			close(b.connected) | ||||
| 			close(b.running) | ||||
| 			return | ||||
| 		case gumble.DisconnectUser: | ||||
| 			b.Log.Infof("Disconnect successful") | ||||
| 			close(b.connected) | ||||
| 			close(b.running) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) doConnect() error { | ||||
| 	// Create new gumble config and attach event handlers | ||||
| 	gumbleConfig := gumble.NewConfig() | ||||
| 	gumbleConfig.Attach(gumbleutil.Listener{ | ||||
| 		ServerConfig: b.handleServerConfig, | ||||
| 		TextMessage:  b.handleTextMessage, | ||||
| 		Connect:      b.handleConnect, | ||||
| 		Disconnect:   b.handleDisconnect, | ||||
| 		UserChange:   b.handleUserChange, | ||||
| 	}) | ||||
| 	gumbleConfig.Username = b.GetString("Nick") | ||||
| 	if password := b.GetString("Password"); password != "" { | ||||
| 		gumbleConfig.Password = password | ||||
| 	} | ||||
|  | ||||
| 	registerNullCodecAsOpus() | ||||
| 	client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	b.client = client | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error { | ||||
| 	channel, ok := client.Channels[channelID] | ||||
| 	if !ok { | ||||
| 		return fmt.Errorf("no channel with ID %d", channelID) | ||||
| 	} | ||||
| 	client.Self.Move(channel) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) doSend() { | ||||
| 	// Message sending loop that makes sure server-side | ||||
| 	// restrictions and client-side message traits don't conflict | ||||
| 	// with each other. | ||||
| 	for { | ||||
| 		select { | ||||
| 		case serverConfig := <-b.serverConfigUpdate: | ||||
| 			b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength) | ||||
| 			b.serverConfig = serverConfig | ||||
| 		case msg := <-b.local: | ||||
| 			b.processMessage(&msg) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bmumble) processMessage(msg *config.Message) { | ||||
| 	b.Log.Debugf("Processing message %s", msg.Text) | ||||
|  | ||||
| 	allowHTML := true | ||||
| 	if b.serverConfig.AllowHTML != nil { | ||||
| 		allowHTML = *b.serverConfig.AllowHTML | ||||
| 	} | ||||
|  | ||||
| 	// If this is a specially generated image message, send it unmodified | ||||
| 	if msg.Event == "mumble_image" { | ||||
| 		if allowHTML { | ||||
| 			b.client.Self.Channel.Send(msg.Username+msg.Text, false) | ||||
| 		} else { | ||||
| 			b.Log.Info("Can't send image, server does not allow HTML messages") | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Don't process empty messages | ||||
| 	if len(msg.Text) == 0 { | ||||
| 		return | ||||
| 	} | ||||
| 	// If HTML is allowed, convert markdown into HTML, otherwise strip markdown | ||||
| 	if allowHTML { | ||||
| 		msg.Text = helper.ParseMarkdown(msg.Text) | ||||
| 	} else { | ||||
| 		msg.Text = stripmd.Strip(msg.Text) | ||||
| 	} | ||||
|  | ||||
| 	// If there is a maximum message length, split and truncate the lines | ||||
| 	var msgLines []string | ||||
| 	if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil { | ||||
| 		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, b.GetString("MessageClipped")) | ||||
| 	} | ||||
| 	// Send the individual lines | ||||
| 	for i := range msgLines { | ||||
| 		// Remove unnecessary newline character, since either way we're sending it as individual lines | ||||
| 		msgLines[i] = strings.TrimSuffix(msgLines[i], "\n") | ||||
| 		b.client.Self.Channel.Send(msg.Username+msgLines[i], false) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										295
									
								
								bridge/nctalk/nctalk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,295 @@ | ||||
| package nctalk | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
|  | ||||
| 	"gomod.garykim.dev/nc-talk/ocs" | ||||
| 	"gomod.garykim.dev/nc-talk/room" | ||||
| 	"gomod.garykim.dev/nc-talk/user" | ||||
| ) | ||||
|  | ||||
| type Btalk struct { | ||||
| 	user  *user.TalkUser | ||||
| 	rooms []Broom | ||||
| 	*bridge.Config | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	return &Btalk{Config: cfg} | ||||
| } | ||||
|  | ||||
| type Broom struct { | ||||
| 	room      *room.TalkRoom | ||||
| 	ctx       context.Context | ||||
| 	ctxCancel context.CancelFunc | ||||
| } | ||||
|  | ||||
| func (b *Btalk) Connect() error { | ||||
| 	b.Log.Info("Connecting") | ||||
| 	tconfig := &user.TalkUserConfig{ | ||||
| 		TLSConfig: &tls.Config{ | ||||
| 			InsecureSkipVerify: b.GetBool("SkipTLSVerify"), //nolint:gosec | ||||
| 		}, | ||||
| 	} | ||||
| 	var err error | ||||
| 	b.user, err = user.NewUser(b.GetString("Server"), b.GetString("Login"), b.GetString("Password"), tconfig) | ||||
| 	if err != nil { | ||||
| 		b.Log.Error("Config could not be used") | ||||
| 		return err | ||||
| 	} | ||||
| 	_, err = b.user.Capabilities() | ||||
| 	if err != nil { | ||||
| 		b.Log.Error("Cannot Connect") | ||||
| 		return err | ||||
| 	} | ||||
| 	b.Log.Info("Connected") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) Disconnect() error { | ||||
| 	for _, r := range b.rooms { | ||||
| 		r.ctxCancel() | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	tr, err := room.NewTalkRoom(b.user, channel.Name) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	newRoom := Broom{ | ||||
| 		room: tr, | ||||
| 	} | ||||
| 	newRoom.ctx, newRoom.ctxCancel = context.WithCancel(context.Background()) | ||||
| 	c, err := newRoom.room.ReceiveMessages(newRoom.ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	b.rooms = append(b.rooms, newRoom) | ||||
|  | ||||
| 	go func() { | ||||
| 		for msg := range c { | ||||
| 			msg := msg | ||||
|  | ||||
| 			if msg.Error != nil { | ||||
| 				b.Log.Errorf("Fatal message poll error: %s\n", msg.Error) | ||||
|  | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// Ignore messages that are from the bot user | ||||
| 			if msg.ActorID == b.user.User || msg.ActorType == "bridged" { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// Handle deleting messages | ||||
| 			if msg.MessageType == ocs.MessageSystem && msg.Parent != nil && msg.Parent.MessageType == ocs.MessageDelete { | ||||
| 				b.handleDeletingMessage(&msg, &newRoom) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// Handle sending messages | ||||
| 			if msg.MessageType == ocs.MessageComment { | ||||
| 				b.handleSendingMessage(&msg, &newRoom) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 		} | ||||
| 	}() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) Send(msg config.Message) (string, error) { | ||||
| 	r := b.getRoom(msg.Channel) | ||||
| 	if r == nil { | ||||
| 		b.Log.Errorf("Could not find room for %v", msg.Channel) | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	// Standard Message Send | ||||
| 	if msg.Event == "" { | ||||
| 		// Handle sending files if they are included | ||||
| 		err := b.handleSendingFile(&msg, r) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("Could not send files in message to room %v from %v: %v", msg.Channel, msg.Username, err) | ||||
|  | ||||
| 			return "", nil | ||||
| 		} | ||||
|  | ||||
| 		sentMessage, err := b.sendText(r, &msg, msg.Text) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err) | ||||
|  | ||||
| 			return "", nil | ||||
| 		} | ||||
| 		return strconv.Itoa(sentMessage.ID), nil | ||||
| 	} | ||||
|  | ||||
| 	// Message Deletion | ||||
| 	if msg.Event == config.EventMsgDelete { | ||||
| 		messageID, err := strconv.Atoi(msg.ID) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		data, err := r.room.DeleteMessage(messageID) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		return strconv.Itoa(data.ID), nil | ||||
| 	} | ||||
|  | ||||
| 	// Message is not a type that is currently supported | ||||
| 	return "", nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) getRoom(token string) *Broom { | ||||
| 	for _, r := range b.rooms { | ||||
| 		if r.room.Token == token { | ||||
| 			return &r | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) sendText(r *Broom, msg *config.Message, text string) (*ocs.TalkRoomMessageData, error) { | ||||
| 	messageToSend := &room.Message{Message: msg.Username + text} | ||||
|  | ||||
| 	if b.GetBool("SeparateDisplayName") { | ||||
| 		messageToSend.Message = text | ||||
| 		messageToSend.ActorDisplayName = msg.Username | ||||
| 	} | ||||
|  | ||||
| 	return r.room.SendComplexMessage(messageToSend) | ||||
| } | ||||
|  | ||||
| func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error { | ||||
| 	for _, parameter := range message.MessageParameters { | ||||
| 		if parameter.Type == ocs.ROSTypeFile { | ||||
| 			// Get the file | ||||
| 			file, err := b.user.DownloadFile(parameter.Path) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			if mmsg.Extra == nil { | ||||
| 				mmsg.Extra = make(map[string][]interface{}) | ||||
| 			} | ||||
|  | ||||
| 			mmsg.Extra["file"] = append(mmsg.Extra["file"], config.FileInfo{ | ||||
| 				Name:   parameter.Name, | ||||
| 				Data:   file, | ||||
| 				Size:   int64(len(*file)), | ||||
| 				Avatar: false, | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) handleSendingFile(msg *config.Message, r *Broom) error { | ||||
| 	for _, f := range msg.Extra["file"] { | ||||
| 		fi := f.(config.FileInfo) | ||||
| 		if fi.URL == "" { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		message := "" | ||||
| 		if fi.Comment != "" { | ||||
| 			message += fi.Comment + " " | ||||
| 		} | ||||
| 		message += fi.URL | ||||
| 		_, err := b.sendText(r, msg, message) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Btalk) handleSendingMessage(msg *ocs.TalkRoomMessageData, r *Broom) { | ||||
| 	remoteMessage := config.Message{ | ||||
| 		Text:     formatRichObjectString(msg.Message, msg.MessageParameters), | ||||
| 		Channel:  r.room.Token, | ||||
| 		Username: DisplayName(msg, b.guestSuffix()), | ||||
| 		UserID:   msg.ActorID, | ||||
| 		Account:  b.Account, | ||||
| 	} | ||||
| 	// It is possible for the ID to not be set on older versions of Talk so we only set it if | ||||
| 	// the ID is not blank | ||||
| 	if msg.ID != 0 { | ||||
| 		remoteMessage.ID = strconv.Itoa(msg.ID) | ||||
| 	} | ||||
|  | ||||
| 	// Handle Files | ||||
| 	err := b.handleFiles(&remoteMessage, msg) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Error handling file: %#v", msg) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("<= Message is %#v", remoteMessage) | ||||
| 	b.Remote <- remoteMessage | ||||
| } | ||||
|  | ||||
| func (b *Btalk) handleDeletingMessage(msg *ocs.TalkRoomMessageData, r *Broom) { | ||||
| 	remoteMessage := config.Message{ | ||||
| 		Event:   config.EventMsgDelete, | ||||
| 		Text:    config.EventMsgDelete, | ||||
| 		Channel: r.room.Token, | ||||
| 		ID:      strconv.Itoa(msg.Parent.ID), | ||||
| 		Account: b.Account, | ||||
| 	} | ||||
| 	b.Log.Debugf("<= Message being deleted is %#v", remoteMessage) | ||||
| 	b.Remote <- remoteMessage | ||||
| } | ||||
|  | ||||
| func (b *Btalk) guestSuffix() string { | ||||
| 	guestSuffix := " (Guest)" | ||||
| 	if b.IsKeySet("GuestSuffix") { | ||||
| 		guestSuffix = b.GetString("GuestSuffix") | ||||
| 	} | ||||
|  | ||||
| 	return guestSuffix | ||||
| } | ||||
|  | ||||
| // Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785 | ||||
| func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string { | ||||
| 	for id, parameter := range parameters { | ||||
| 		text := parameter.Name | ||||
|  | ||||
| 		switch parameter.Type { | ||||
| 		case ocs.ROSTypeUser, ocs.ROSTypeGroup: | ||||
| 			text = "@" + text | ||||
| 		case ocs.ROSTypeFile: | ||||
| 			if parameter.Link != "" { | ||||
| 				text = parameter.Name | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		message = strings.ReplaceAll(message, "{"+id+"}", text) | ||||
| 	} | ||||
|  | ||||
| 	return message | ||||
| } | ||||
|  | ||||
| func DisplayName(msg *ocs.TalkRoomMessageData, suffix string) string { | ||||
| 	if msg.ActorType == ocs.ActorGuest { | ||||
| 		if msg.ActorDisplayName == "" { | ||||
| 			return "Guest" | ||||
| 		} | ||||
|  | ||||
| 		return msg.ActorDisplayName + suffix | ||||
| 	} | ||||
|  | ||||
| 	return msg.ActorDisplayName | ||||
| } | ||||
| @@ -1,7 +1,11 @@ | ||||
| package brocketchat | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/matterbridge/Rocket.Chat.Go.SDK/models" | ||||
| ) | ||||
|  | ||||
| func (b *Brocketchat) handleRocket() { | ||||
| @@ -38,8 +42,26 @@ func (b *Brocketchat) handleRocketHook(messages chan *config.Message) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Brocketchat) handleStatusEvent(ev models.Message, rmsg *config.Message) bool { | ||||
| 	switch ev.Type { | ||||
| 	case "": | ||||
| 		// this is a normal message, no processing needed | ||||
| 		// return true so the message is not dropped | ||||
| 		return true | ||||
| 	case sUserJoined, sUserLeft: | ||||
| 		rmsg.Event = config.EventJoinLeave | ||||
| 		return true | ||||
| 	case sRoomChangedTopic: | ||||
| 		rmsg.Event = config.EventTopicChange | ||||
| 		return true | ||||
| 	} | ||||
| 	b.Log.Debugf("Dropping message with unknown type: %s", ev.Type) | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (b *Brocketchat) handleRocketClient(messages chan *config.Message) { | ||||
| 	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 | ||||
| @@ -58,11 +80,51 @@ 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) { | ||||
| 			messages <- rmsg | ||||
| 		} | ||||
| 		messages <- rmsg | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Brocketchat) handleAttachments(message *models.Message, rmsg *config.Message) { | ||||
| 	if rmsg.Text == "" { | ||||
| 		for _, attachment := range message.Attachments { | ||||
| 			if attachment.Title != "" { | ||||
| 				rmsg.Text = attachment.Title + "\n" | ||||
| 			} | ||||
| 			if attachment.Title != "" && attachment.Text != "" { | ||||
| 				rmsg.Text += "\n" | ||||
| 			} | ||||
| 			if attachment.Text != "" { | ||||
| 				rmsg.Text += attachment.Text | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for i := range message.Attachments { | ||||
| 		if err := b.handleDownloadFile(rmsg, &message.Attachments[i]); err != nil { | ||||
| 			b.Log.Errorf("Could not download incoming file: %#v", err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Brocketchat) handleDownloadFile(rmsg *config.Message, file *models.Attachment) error { | ||||
| 	downloadURL := b.GetString("server") + file.TitleLink | ||||
| 	data, err := helper.DownloadFileAuthRocket(downloadURL, b.user.Token, b.user.ID) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("download %s failed %#v", downloadURL, err) | ||||
| 	} | ||||
| 	helper.HandleDownloadData(b.Log, rmsg, file.Title, rmsg.Text, downloadURL, data, b.General) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Brocketchat) handleUploadFile(msg *config.Message) error { | ||||
| 	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 | ||||
|   | ||||
| @@ -29,6 +29,12 @@ type Brocketchat struct { | ||||
| 	sync.RWMutex | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	sUserJoined       = "uj" | ||||
| 	sUserLeft         = "ul" | ||||
| 	sRoomChangedTopic = "room_changed_topic" | ||||
| ) | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	newCache, err := lru.New(100) | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -16,7 +16,7 @@ var ErrEventIgnored = errors.New("this event message should ignored") | ||||
|  | ||||
| func (b *Bslack) handleSlack() { | ||||
| 	messages := make(chan *config.Message) | ||||
| 	if b.GetString(incomingWebhookConfig) != "" { | ||||
| 	if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" { | ||||
| 		b.Log.Debugf("Choosing webhooks based receiving") | ||||
| 		go b.handleMatterHook(messages) | ||||
| 	} else { | ||||
| @@ -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) | ||||
| 		} | ||||
| @@ -137,12 +147,6 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { | ||||
| 		hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid | ||||
| 	} | ||||
|  | ||||
| 	// Skip any messages that we made ourselves or from 'slackbot' (see #527). | ||||
| 	if ev.Username == sSlackBotUser || | ||||
| 		(b.rtm != nil && ev.Username == b.si.User.Name) || hasOurCallbackID { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	if ev.SubMessage != nil { | ||||
| 		// It seems ev.SubMessage.Edited == nil when slack unfurls. | ||||
| 		// Do not forward these messages. See Github issue #266. | ||||
| @@ -155,6 +159,16 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { | ||||
| 		if ev.SubType == "message_replied" && ev.Hidden { | ||||
| 			return true | ||||
| 		} | ||||
| 		if len(ev.SubMessage.Blocks.BlockSet) == 1 { | ||||
| 			block, ok := ev.SubMessage.Blocks.BlockSet[0].(*slack.SectionBlock) | ||||
| 			hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Skip any messages that we made ourselves or from 'slackbot' (see #527). | ||||
| 	if ev.Username == sSlackBotUser || | ||||
| 		(b.rtm != nil && ev.Username == b.si.User.Name) || hasOurCallbackID { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	if len(ev.Files) > 0 { | ||||
| @@ -216,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: | ||||
| @@ -248,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 { | ||||
| @@ -256,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 | ||||
| 			} | ||||
| @@ -275,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) | ||||
| 		} | ||||
| @@ -324,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" | ||||
| @@ -64,6 +65,7 @@ const ( | ||||
| 	editSuffixConfig      = "EditSuffix" | ||||
| 	iconURLConfig         = "iconurl" | ||||
| 	noSendJoinConfig      = "nosendjoinpart" | ||||
| 	messageLength         = 3000 | ||||
| ) | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| @@ -155,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": | ||||
| @@ -194,6 +196,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) { | ||||
| 		b.Log.Debugf("=> Receiving %#v", msg) | ||||
| 	} | ||||
|  | ||||
| 	msg.Text = helper.ClipMessage(msg.Text, messageLength, b.GetString("MessageClipped")) | ||||
| 	msg.Text = b.replaceCodeFence(msg.Text) | ||||
|  | ||||
| 	// Make a action /me of the message | ||||
| @@ -202,7 +205,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) { | ||||
| 	} | ||||
|  | ||||
| 	// Use webhook to send the message | ||||
| 	if b.GetString(outgoingWebhookConfig) != "" { | ||||
| 	if b.GetString(outgoingWebhookConfig) != "" && b.GetString(tokenConfig) == "" { | ||||
| 		return "", b.sendWebhook(msg) | ||||
| 	} | ||||
| 	return b.sendRTM(msg) | ||||
| @@ -297,7 +300,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) { | ||||
| 	} | ||||
|  | ||||
| 	// Handle prefix hint for unthreaded messages. | ||||
| 	if msg.ParentID == "msg-parent-not-found" { | ||||
| 	if msg.ParentNotFound() { | ||||
| 		msg.ParentID = "" | ||||
| 		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) | ||||
| 	} | ||||
| @@ -318,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] | ||||
| @@ -329,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. | ||||
| @@ -440,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 { | ||||
| @@ -457,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), | ||||
| @@ -468,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 | ||||
| @@ -39,32 +58,54 @@ func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message | ||||
|  | ||||
| // handleForwarded handles forwarded messages | ||||
| func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) { | ||||
| 	if message.ForwardFrom != nil { | ||||
| 		usernameForward := "" | ||||
| 		if b.GetBool("UseFirstName") { | ||||
| 	if message.ForwardDate == 0 { | ||||
| 		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 | ||||
| 	} | ||||
|  | ||||
| 	usernameForward := "" | ||||
| 	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 | ||||
| 		if usernameForward == "" { | ||||
| 			usernameForward = message.ForwardFrom.FirstName | ||||
| 		} | ||||
| 		if usernameForward == "" { | ||||
| 			usernameForward = message.ForwardFrom.UserName | ||||
| 			if usernameForward == "" { | ||||
| 				usernameForward = message.ForwardFrom.FirstName | ||||
| 			} | ||||
| 		} | ||||
| 		if usernameForward == "" { | ||||
| 			usernameForward = unknownUser | ||||
| 		} | ||||
| 		rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text | ||||
| 	} | ||||
|  | ||||
| 	if usernameForward == "" { | ||||
| 		usernameForward = unknownUser | ||||
| 	} | ||||
|  | ||||
| 	rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text | ||||
| } | ||||
|  | ||||
| // handleQuoting handles quoting of previous messages | ||||
| 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 == "" { | ||||
| @@ -76,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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -84,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 == "" { | ||||
| @@ -100,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 | ||||
| @@ -112,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{})} | ||||
| @@ -134,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) | ||||
| @@ -150,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) | ||||
| @@ -167,54 +263,132 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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 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{})} | ||||
| 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.Itoa(userid)]; !ok { | ||||
| 		photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1}) | ||||
| 	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.Errorf("Userprofile download failed for %#v %s", userid, err) | ||||
| 			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 | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 		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) | ||||
| func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) { | ||||
| 	format := b.GetString("MediaConvertTgs") | ||||
| 	if helper.SupportsFormat(format) { | ||||
| 		b.Log.Debugf("Format supported by %s, converting %v", helper.LottieBackend(), name) | ||||
| 	} else { | ||||
| 		// Otherwise, no conversion was requested. Trying to run the usual webp | ||||
| 		// converter would fail, because '.tgs.webp' is actually a gzipped JSON | ||||
| 		// file, and has nothing to do with WebP. | ||||
| 		return | ||||
| 	} | ||||
| 	err := helper.ConvertTgsToX(data, format, b.Log) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("conversion failed: %v", err) | ||||
| 	} else { | ||||
| 		*name = strings.Replace(*name, "tgs.webp", format, 1) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 			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) maybeConvertWebp(name *string, data *[]byte) { | ||||
| 	if b.GetBool("MediaConvertWebPToPNG") { | ||||
| 		b.Log.Debugf("WebP to PNG conversion enabled, converting %v", name) | ||||
| 		err := helper.ConvertWebPToPNG(data) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("conversion failed: %v", err) | ||||
| 		} else { | ||||
| 			*name = strings.Replace(*name, ".webp", ".png", 1) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // handleDownloadFile handles file download | ||||
| 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 | ||||
| @@ -230,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) | ||||
| 	} | ||||
|  | ||||
| @@ -254,15 +428,18 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if strings.HasSuffix(name, ".webp") && b.GetBool("MediaConvertWebPToPNG") { | ||||
| 		b.Log.Debugf("WebP to PNG conversion enabled, converting %s", name) | ||||
| 		err := helper.ConvertWebPToPNG(data) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("conversion failed: %s", err) | ||||
| 		} else { | ||||
| 			name = strings.Replace(name, ".webp", ".png", 1) | ||||
| 		} | ||||
|  | ||||
| 	if strings.HasSuffix(name, ".tgs.webp") { | ||||
| 		b.maybeConvertTgs(&name, data) | ||||
| 	} else if strings.HasSuffix(name, ".webp") { | ||||
| 		b.maybeConvertWebp(&name, data) | ||||
| 	} | ||||
|  | ||||
| 	// rename .oga to .ogg  https://github.com/42wim/matterbridge/issues/906#issuecomment-741793512 | ||||
| 	if strings.HasSuffix(name, ".oga") && message.Audio != nil { | ||||
| 		name = strings.Replace(name, ".oga", ".ogg", 1) | ||||
| 	} | ||||
|  | ||||
| 	helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General) | ||||
| 	return nil | ||||
| } | ||||
| @@ -274,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 | ||||
| @@ -286,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 | ||||
| } | ||||
|  | ||||
| @@ -312,6 +493,9 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error | ||||
| 	case "Markdown": | ||||
| 		b.Log.Debug("Using mode markdown") | ||||
| 		m.ParseMode = tgbotapi.ModeMarkdown | ||||
| 	case 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") | ||||
| @@ -325,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|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 { | ||||
| @@ -357,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")]) | ||||
| @@ -376,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,20 +1,23 @@ | ||||
| package btelegram | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"html" | ||||
| 	"log" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"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 ( | ||||
| 	unknownUser = "unknown" | ||||
| 	HTMLFormat  = "HTML" | ||||
| 	HTMLNick    = "htmlnick" | ||||
| 	MarkdownV2  = "MarkdownV2" | ||||
| ) | ||||
|  | ||||
| type Btelegram struct { | ||||
| @@ -24,6 +27,16 @@ type Btelegram struct { | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	tgsConvertFormat := cfg.GetString("MediaConvertTgs") | ||||
| 	if tgsConvertFormat != "" { | ||||
| 		err := helper.CanConvertTgsToX() | ||||
| 		if err != nil { | ||||
| 			log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s does not appear to work:\n%#v", tgsConvertFormat, helper.LottieBackend(), err) | ||||
| 		} | ||||
| 		if !helper.SupportsFormat(tgsConvertFormat) { | ||||
| 			log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s doesn't support it.", tgsConvertFormat, helper.LottieBackend()) | ||||
| 		} | ||||
| 	} | ||||
| 	return &Btelegram{Config: cfg, avatarMap: make(map[string]string)} | ||||
| } | ||||
|  | ||||
| @@ -37,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 | ||||
| @@ -55,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 | ||||
| 	} | ||||
| @@ -70,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 | ||||
| @@ -78,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) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -101,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 | ||||
| @@ -115,23 +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 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) | ||||
| @@ -141,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, | ||||
|   | ||||
							
								
								
									
										333
									
								
								bridge/vk/vk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,333 @@ | ||||
| package bvk | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"regexp" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge" | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
|  | ||||
| 	"github.com/SevereCloud/vksdk/v2/api" | ||||
| 	"github.com/SevereCloud/vksdk/v2/events" | ||||
| 	longpoll "github.com/SevereCloud/vksdk/v2/longpoll-bot" | ||||
| 	"github.com/SevereCloud/vksdk/v2/object" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	audioMessage = "audio_message" | ||||
| 	document     = "doc" | ||||
| 	photo        = "photo" | ||||
| 	video        = "video" | ||||
| 	graffiti     = "graffiti" | ||||
| 	sticker      = "sticker" | ||||
| 	wall         = "wall" | ||||
| ) | ||||
|  | ||||
| type user struct { | ||||
| 	lastname, firstname, avatar string | ||||
| } | ||||
|  | ||||
| type Bvk struct { | ||||
| 	c            *api.VK | ||||
| 	lp           *longpoll.LongPoll | ||||
| 	usernamesMap map[int]user // cache of user names and avatar URLs | ||||
| 	*bridge.Config | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	return &Bvk{usernamesMap: make(map[int]user), Config: cfg} | ||||
| } | ||||
|  | ||||
| func (b *Bvk) Connect() error { | ||||
| 	b.Log.Info("Connecting") | ||||
| 	b.c = api.NewVK(b.GetString("Token")) | ||||
|  | ||||
| 	var err error | ||||
| 	b.lp, err = longpoll.NewLongPollCommunity(b.c) | ||||
| 	if err != nil { | ||||
| 		b.Log.Debugf("%#v", err) | ||||
|  | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) { | ||||
| 		b.handleMessage(obj.Message, false) | ||||
| 	}) | ||||
|  | ||||
| 	b.Log.Info("Connection succeeded") | ||||
|  | ||||
| 	go func() { | ||||
| 		err := b.lp.Run() | ||||
| 		if err != nil { | ||||
| 			b.Log.WithError(err).Fatal("Enable longpoll in group management") | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bvk) Disconnect() error { | ||||
| 	b.lp.Shutdown() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bvk) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bvk) Send(msg config.Message) (string, error) { | ||||
| 	b.Log.Debugf("=> Receiving %#v", msg) | ||||
|  | ||||
| 	peerID, err := strconv.Atoi(msg.Channel) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	params := api.Params{} | ||||
|  | ||||
| 	text := msg.Username + msg.Text | ||||
|  | ||||
| 	if msg.Extra != nil { | ||||
| 		if len(msg.Extra["file"]) > 0 { | ||||
| 			// generate attachments string | ||||
| 			attachment, urls := b.uploadFiles(msg.Extra, peerID) | ||||
| 			params["attachment"] = attachment | ||||
| 			text += urls | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	params["message"] = text | ||||
|  | ||||
| 	if msg.ID == "" { | ||||
| 		// New message | ||||
| 		params["random_id"] = time.Now().Unix() | ||||
| 		params["peer_ids"] = msg.Channel | ||||
|  | ||||
| 		res, e := b.c.MessagesSendPeerIDs(params) | ||||
| 		if e != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		return strconv.Itoa(res[0].ConversationMessageID), nil | ||||
| 	} | ||||
| 	// Edit message | ||||
| 	messageID, err := strconv.ParseInt(msg.ID, 10, 64) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	params["peer_id"] = peerID | ||||
| 	params["conversation_message_id"] = messageID | ||||
|  | ||||
| 	_, err = b.c.MessagesEdit(params) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return msg.ID, nil | ||||
| } | ||||
|  | ||||
| func (b *Bvk) getUser(id int) user { | ||||
| 	u, found := b.usernamesMap[id] | ||||
| 	if !found { | ||||
| 		b.Log.Debug("Fetching username for ", id) | ||||
|  | ||||
| 		if id >= 0 { | ||||
| 			result, _ := b.c.UsersGet(api.Params{ | ||||
| 				"user_ids": id, | ||||
| 				"fields":   "photo_200", | ||||
| 			}) | ||||
|  | ||||
| 			resUser := result[0] | ||||
| 			u = user{lastname: resUser.LastName, firstname: resUser.FirstName, avatar: resUser.Photo200} | ||||
| 			b.usernamesMap[id] = u | ||||
| 		} else { | ||||
| 			result, _ := b.c.GroupsGetByID(api.Params{ | ||||
| 				"group_id": id * -1, | ||||
| 			}) | ||||
|  | ||||
| 			resGroup := result[0] | ||||
| 			u = user{lastname: resGroup.Name, avatar: resGroup.Photo200} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return u | ||||
| } | ||||
|  | ||||
| func (b *Bvk) handleMessage(msg object.MessagesMessage, isFwd bool) { | ||||
| 	b.Log.Debug("ChatID: ", msg.PeerID) | ||||
| 	// fetch user info | ||||
| 	u := b.getUser(msg.FromID) | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		Text:     msg.Text, | ||||
| 		Username: u.firstname + " " + u.lastname, | ||||
| 		Avatar:   u.avatar, | ||||
| 		Channel:  strconv.Itoa(msg.PeerID), | ||||
| 		Account:  b.Account, | ||||
| 		UserID:   strconv.Itoa(msg.FromID), | ||||
| 		ID:       strconv.Itoa(msg.ConversationMessageID), | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 	} | ||||
|  | ||||
| 	if msg.ReplyMessage != nil { | ||||
| 		ur := b.getUser(msg.ReplyMessage.FromID) | ||||
| 		rmsg.Text = "Re: " + ur.firstname + " " + ur.lastname + "\n" + rmsg.Text | ||||
| 	} | ||||
|  | ||||
| 	if isFwd { | ||||
| 		rmsg.Username = "Fwd: " + rmsg.Username | ||||
| 	} | ||||
|  | ||||
| 	if len(msg.Attachments) > 0 { | ||||
| 		urls, text := b.getFiles(msg.Attachments) | ||||
|  | ||||
| 		if text != "" { | ||||
| 			rmsg.Text += "\n" + text | ||||
| 		} | ||||
|  | ||||
| 		// download | ||||
| 		b.downloadFiles(&rmsg, urls) | ||||
| 	} | ||||
|  | ||||
| 	if len(msg.FwdMessages) > 0 { | ||||
| 		rmsg.Text += strconv.Itoa(len(msg.FwdMessages)) + " forwarded messages" | ||||
| 	} | ||||
|  | ||||
| 	b.Remote <- rmsg | ||||
|  | ||||
| 	if len(msg.FwdMessages) > 0 { | ||||
| 		// recursive processing of forwarded messages | ||||
| 		for _, m := range msg.FwdMessages { | ||||
| 			m.PeerID = msg.PeerID | ||||
| 			b.handleMessage(m, true) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, string) { | ||||
| 	var attachments []string | ||||
| 	text := "" | ||||
|  | ||||
| 	for _, f := range extra["file"] { | ||||
| 		fi := f.(config.FileInfo) | ||||
|  | ||||
| 		if fi.Comment != "" { | ||||
| 			text += fi.Comment + "\n" | ||||
| 		} | ||||
| 		a, err := b.uploadFile(fi, peerID) | ||||
| 		if err != nil { | ||||
| 			b.Log.WithError(err).Error("File upload error ", fi.Name) | ||||
| 		} | ||||
|  | ||||
| 		attachments = append(attachments, a) | ||||
| 	} | ||||
|  | ||||
| 	return strings.Join(attachments, ","), text | ||||
| } | ||||
|  | ||||
| func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) { | ||||
| 	r := bytes.NewReader(*file.Data) | ||||
|  | ||||
| 	photoRE := regexp.MustCompile(".(jpg|jpe|png)$") | ||||
| 	if photoRE.MatchString(file.Name) { | ||||
| 		// BUG(VK): for community chat peerID=0 | ||||
| 		p, err := b.c.UploadMessagesPhoto(0, r) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		return photo + strconv.Itoa(p[0].OwnerID) + "_" + strconv.Itoa(p[0].ID), nil | ||||
| 	} | ||||
|  | ||||
| 	var doctype string | ||||
| 	if strings.Contains(file.Name, ".ogg") { | ||||
| 		doctype = audioMessage | ||||
| 	} else { | ||||
| 		doctype = document | ||||
| 	} | ||||
|  | ||||
| 	doc, err := b.c.UploadMessagesDoc(peerID, doctype, file.Name, "", r) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	switch doc.Type { | ||||
| 	case audioMessage: | ||||
| 		return document + strconv.Itoa(doc.AudioMessage.OwnerID) + "_" + strconv.Itoa(doc.AudioMessage.ID), nil | ||||
| 	case document: | ||||
| 		return document + strconv.Itoa(doc.Doc.OwnerID) + "_" + strconv.Itoa(doc.Doc.ID), nil | ||||
| 	} | ||||
|  | ||||
| 	return "", nil | ||||
| } | ||||
|  | ||||
| func (b *Bvk) getFiles(attachments []object.MessagesMessageAttachment) ([]string, string) { | ||||
| 	var urls []string | ||||
| 	var text []string | ||||
|  | ||||
| 	for _, a := range attachments { | ||||
| 		switch a.Type { | ||||
| 		case photo: | ||||
| 			var resolution float64 = 0 | ||||
| 			url := a.Photo.Sizes[0].URL | ||||
| 			for _, size := range a.Photo.Sizes { | ||||
| 				r := size.Height * size.Width | ||||
| 				if resolution < r { | ||||
| 					resolution = r | ||||
| 					url = size.URL | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			urls = append(urls, url) | ||||
|  | ||||
| 		case document: | ||||
| 			urls = append(urls, a.Doc.URL) | ||||
|  | ||||
| 		case graffiti: | ||||
| 			urls = append(urls, a.Graffiti.URL) | ||||
|  | ||||
| 		case audioMessage: | ||||
| 			urls = append(urls, a.AudioMessage.DocsDocPreviewAudioMessage.LinkOgg) | ||||
|  | ||||
| 		case sticker: | ||||
| 			var resolution float64 = 0 | ||||
| 			url := a.Sticker.Images[0].URL | ||||
| 			for _, size := range a.Sticker.Images { | ||||
| 				r := size.Height * size.Width | ||||
| 				if resolution < r { | ||||
| 					resolution = r | ||||
| 					url = size.URL | ||||
| 				} | ||||
| 			} | ||||
| 			urls = append(urls, url+".png") | ||||
| 		case video: | ||||
| 			text = append(text, "https://vk.com/video"+strconv.Itoa(a.Video.OwnerID)+"_"+strconv.Itoa(a.Video.ID)) | ||||
|  | ||||
| 		case wall: | ||||
| 			text = append(text, "https://vk.com/wall"+strconv.Itoa(a.Wall.FromID)+"_"+strconv.Itoa(a.Wall.ID)) | ||||
|  | ||||
| 		default: | ||||
| 			text = append(text, "This attachment is not supported ("+a.Type+")") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return urls, strings.Join(text, "\n") | ||||
| } | ||||
|  | ||||
| func (b *Bvk) downloadFiles(rmsg *config.Message, urls []string) { | ||||
| 	for _, url := range urls { | ||||
| 		data, err := helper.DownloadFile(url) | ||||
| 		if err == nil { | ||||
| 			urlPart := strings.Split(url, "/") | ||||
| 			name := strings.Split(urlPart[len(urlPart)-1], "?")[0] | ||||
| 			helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,3 +1,4 @@ | ||||
| // nolint:goconst | ||||
| package bwhatsapp | ||||
|  | ||||
| import ( | ||||
| @@ -23,7 +24,9 @@ Check: | ||||
| // HandleError received from WhatsApp | ||||
| func (b *Bwhatsapp) HandleError(err error) { | ||||
| 	// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843 | ||||
| 	if strings.Contains(err.Error(), "error processing data: received invalid data") { | ||||
| 	// ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094 | ||||
| 	if strings.Contains(err.Error(), "error processing data: received invalid data") || | ||||
| 		strings.Contains(err.Error(), "invalid string with tag 174") { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -46,16 +49,22 @@ func (b *Bwhatsapp) reconnect(err error) { | ||||
| 		Max:    5 * time.Minute, | ||||
| 		Jitter: true, | ||||
| 	} | ||||
|  | ||||
| 	for { | ||||
| 		d := bf.Duration() | ||||
|  | ||||
| 		b.Log.Errorf("Connection failed, underlying error: %v", err) | ||||
| 		b.Log.Infof("Waiting %s...", d) | ||||
|  | ||||
| 		time.Sleep(d) | ||||
|  | ||||
| 		b.Log.Info("Reconnecting...") | ||||
|  | ||||
| 		err := b.conn.Restore() | ||||
| 		if err == nil { | ||||
| 			bf.Reset() | ||||
| 			b.startedAt = uint64(time.Now().Unix()) | ||||
|  | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| @@ -63,7 +72,7 @@ func (b *Bwhatsapp) reconnect(err error) { | ||||
|  | ||||
| // HandleTextMessage sent from WhatsApp, relay it to the brige | ||||
| func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { | ||||
| 	if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") { | ||||
| 	if message.Info.FromMe { | ||||
| 		return | ||||
| 	} | ||||
| 	// whatsapp sends last messages to show context , cut them | ||||
| @@ -71,13 +80,13 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones | ||||
| 	groupJID := message.Info.RemoteJid | ||||
|  | ||||
| 	senderJID := message.Info.SenderJid | ||||
|  | ||||
| 	if len(senderJID) == 0 { | ||||
| 		// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved | ||||
| 		senderJID = *message.Info.Source.Participant | ||||
| 		if message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||
| 			senderJID = *message.Info.Source.Participant | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// translate sender's JID to the nicest username we can get | ||||
| @@ -98,108 +107,276 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { | ||||
| 			if mention == "" { | ||||
| 				mention = "someone" | ||||
| 			} | ||||
|  | ||||
| 			message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:    senderJID, | ||||
| 		Username:  senderName, | ||||
| 		Text:      message.Text, | ||||
| 		Timestamp: messageTime, | ||||
| 		Channel:   groupJID, | ||||
| 		Account:   b.Account, | ||||
| 		Protocol:  b.Protocol, | ||||
| 		Extra:     make(map[string][]interface{}), | ||||
| 		UserID:   senderJID, | ||||
| 		Username: senderName, | ||||
| 		Text:     message.Text, | ||||
| 		Channel:  groupJID, | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 		//	ParentID: TODO, // TODO handle thread replies  // map from Info.QuotedMessageID string | ||||
| 		//	Event     string    `json:"event"` | ||||
| 		//	Gateway   string  // will be added during message processing | ||||
| 		ID: message.Info.Id} | ||||
| 		ID: message.Info.Id, | ||||
| 	} | ||||
|  | ||||
| 	if avatarURL, exists := b.userAvatars[senderJID]; 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 | ||||
| // nolint:funlen | ||||
| func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) { | ||||
| 	if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") { | ||||
| 	if message.Info.FromMe || message.Info.Timestamp < b.startedAt { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// whatsapp sends last messages to show context , cut them | ||||
| 	if message.Info.Timestamp < b.startedAt { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones | ||||
| 	groupJID := message.Info.RemoteJid | ||||
|  | ||||
| 	senderJID := message.Info.SenderJid | ||||
| 	// if len(senderJid) == 0 { | ||||
| 	//   // TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved | ||||
| 	//   senderJid = *message.Info.Source.Participant | ||||
| 	// } | ||||
| 	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||
| 		senderJID = *message.Info.Source.Participant | ||||
| 	} | ||||
|  | ||||
| 	// translate sender's Jid to the nicest username we can get | ||||
| 	senderName := b.getSenderName(senderJID) | ||||
| 	senderName := b.getSenderName(message.Info.SenderJid) | ||||
| 	if senderName == "" { | ||||
| 		senderName = "Someone" // don't expose telephone number | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:    senderJID, | ||||
| 		Username:  senderName, | ||||
| 		Timestamp: messageTime, | ||||
| 		Channel:   groupJID, | ||||
| 		Account:   b.Account, | ||||
| 		Protocol:  b.Protocol, | ||||
| 		Extra:     make(map[string][]interface{}), | ||||
| 		//  ParentID: TODO,      // TODO handle thread replies  // map from Info.QuotedMessageID string | ||||
| 		//  Event     string    `json:"event"` | ||||
| 		//  Gateway   string     // will be added during message processing | ||||
| 		ID: message.Info.Id} | ||||
| 		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 | ||||
| 	} | ||||
|  | ||||
| 	// Download and unencrypt content | ||||
| 	data, err := message.Download() | ||||
| 	fileExt, err := mime.ExtensionsByType(message.Type) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("%v", err) | ||||
| 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Get file extension by mimetype | ||||
| 	fileExt, err := mime.ExtensionsByType(message.Type) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("%v", err) | ||||
| 		return | ||||
| 	// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292 | ||||
| 	if fileExt[0] == ".jfif" { | ||||
| 		fileExt[0] = ".jpg" | ||||
| 	} | ||||
|  | ||||
| 	// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463 | ||||
| 	if fileExt[0] == ".jpe" { | ||||
| 		fileExt[0] = ".jpg" | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0]) | ||||
|  | ||||
| 	b.Log.Debugf("<= Image downloaded and unencrypted") | ||||
| 	b.Log.Debugf("Trying to download %s with type %s", filename, message.Type) | ||||
|  | ||||
| 	data, err := message.Download() | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Download image failed: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Move file to bridge storage | ||||
| 	helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General) | ||||
|  | ||||
| 	b.Log.Debugf("<= Image Message is %#v", rmsg) | ||||
| 	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) HandleVideoMessage(message whatsapp.VideoMessage) { | ||||
| //	fmt.Println(message) // TODO implement | ||||
| //} | ||||
| // | ||||
| //func (b *Bwhatsapp) HandleJsonMessage(message string) { | ||||
| //	fmt.Println(message) // TODO implement | ||||
| //} | ||||
| // TODO HandleRawMessage | ||||
| // TODO HandleAudioMessage | ||||
| // HandleVideoMessage downloads video messages | ||||
| func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) { | ||||
| 	if message.Info.FromMe || message.Info.Timestamp < b.startedAt { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	senderJID := message.Info.SenderJid | ||||
| 	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||
| 		senderJID = *message.Info.Source.Participant | ||||
| 	} | ||||
|  | ||||
| 	senderName := b.getSenderName(message.Info.SenderJid) | ||||
| 	if senderName == "" { | ||||
| 		senderName = "Someone" // don't expose telephone number | ||||
| 	} | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   senderJID, | ||||
| 		Username: senderName, | ||||
| 		Channel:  message.Info.RemoteJid, | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 		ID:       message.Info.Id, | ||||
| 	} | ||||
|  | ||||
| 	if avatarURL, exists := b.userAvatars[senderJID]; exists { | ||||
| 		rmsg.Avatar = avatarURL | ||||
| 	} | ||||
|  | ||||
| 	fileExt, err := mime.ExtensionsByType(message.Type) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(fileExt) == 0 { | ||||
| 		fileExt = append(fileExt, ".mp4") | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0]) | ||||
|  | ||||
| 	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type) | ||||
|  | ||||
| 	data, err := message.Download() | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Download video failed: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Move file to bridge storage | ||||
| 	helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General) | ||||
|  | ||||
| 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||
|  | ||||
| 	b.Remote <- rmsg | ||||
| } | ||||
|  | ||||
| // HandleAudioMessage downloads audio messages | ||||
| func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) { | ||||
| 	if message.Info.FromMe || message.Info.Timestamp < b.startedAt { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	senderJID := message.Info.SenderJid | ||||
| 	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||
| 		senderJID = *message.Info.Source.Participant | ||||
| 	} | ||||
|  | ||||
| 	senderName := b.getSenderName(message.Info.SenderJid) | ||||
| 	if senderName == "" { | ||||
| 		senderName = "Someone" // don't expose telephone number | ||||
| 	} | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   senderJID, | ||||
| 		Username: senderName, | ||||
| 		Channel:  message.Info.RemoteJid, | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 		ID:       message.Info.Id, | ||||
| 	} | ||||
|  | ||||
| 	if avatarURL, exists := b.userAvatars[senderJID]; exists { | ||||
| 		rmsg.Avatar = avatarURL | ||||
| 	} | ||||
|  | ||||
| 	fileExt, err := mime.ExtensionsByType(message.Type) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(fileExt) == 0 { | ||||
| 		fileExt = append(fileExt, ".ogg") | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0]) | ||||
|  | ||||
| 	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type) | ||||
|  | ||||
| 	data, err := message.Download() | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Download audio failed: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Move file to bridge storage | ||||
| 	helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General) | ||||
|  | ||||
| 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||
|  | ||||
| 	b.Remote <- rmsg | ||||
| } | ||||
|  | ||||
| // HandleDocumentMessage downloads documents | ||||
| func (b *Bwhatsapp) HandleDocumentMessage(message whatsapp.DocumentMessage) { | ||||
| 	if message.Info.FromMe || message.Info.Timestamp < b.startedAt { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	senderJID := message.Info.SenderJid | ||||
| 	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||
| 		senderJID = *message.Info.Source.Participant | ||||
| 	} | ||||
|  | ||||
| 	senderName := b.getSenderName(message.Info.SenderJid) | ||||
| 	if senderName == "" { | ||||
| 		senderName = "Someone" // don't expose telephone number | ||||
| 	} | ||||
|  | ||||
| 	rmsg := config.Message{ | ||||
| 		UserID:   senderJID, | ||||
| 		Username: senderName, | ||||
| 		Channel:  message.Info.RemoteJid, | ||||
| 		Account:  b.Account, | ||||
| 		Protocol: b.Protocol, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 		ID:       message.Info.Id, | ||||
| 	} | ||||
|  | ||||
| 	if avatarURL, exists := b.userAvatars[senderJID]; exists { | ||||
| 		rmsg.Avatar = avatarURL | ||||
| 	} | ||||
|  | ||||
| 	fileExt, err := mime.ExtensionsByType(message.Type) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("%v", message.FileName) | ||||
|  | ||||
| 	b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, message.Type) | ||||
|  | ||||
| 	data, err := message.Download() | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Download document message failed: %s", err) | ||||
|  | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Move file to bridge storage | ||||
| 	helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General) | ||||
|  | ||||
| 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||
|  | ||||
| 	b.Remote <- rmsg | ||||
| } | ||||
|   | ||||
| @@ -6,22 +6,24 @@ import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
|  | ||||
| 	qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go" | ||||
| 	"github.com/Rhymen/go-whatsapp" | ||||
| ) | ||||
|  | ||||
| type ProfilePicInfo struct { | ||||
| 	URL string `json:"eurl"` | ||||
| 	Tag string `json:"tag"` | ||||
|  | ||||
| 	Status int16 `json:"status"` | ||||
| 	URL    string `json:"eurl"` | ||||
| 	Tag    string `json:"tag"` | ||||
| 	Status int16  `json:"status"` | ||||
| } | ||||
|  | ||||
| func qrFromTerminal(invert bool) chan string { | ||||
| 	qr := make(chan string) | ||||
|  | ||||
| 	go func() { | ||||
| 		terminal := qrcodeTerminal.New() | ||||
|  | ||||
| 		if invert { | ||||
| 			terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium) | ||||
| 		} | ||||
| @@ -44,13 +46,12 @@ func (b *Bwhatsapp) readSession() (whatsapp.Session, error) { | ||||
| 	if err != nil { | ||||
| 		return session, err | ||||
| 	} | ||||
|  | ||||
| 	defer file.Close() | ||||
|  | ||||
| 	decoder := gob.NewDecoder(file) | ||||
| 	err = decoder.Decode(&session) | ||||
| 	if err != nil { | ||||
| 		return session, err | ||||
| 	} | ||||
| 	return session, nil | ||||
|  | ||||
| 	return session, decoder.Decode(&session) | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) writeSession(session whatsapp.Session) error { | ||||
| @@ -65,11 +66,31 @@ func (b *Bwhatsapp) writeSession(session whatsapp.Session) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer file.Close() | ||||
| 	encoder := gob.NewEncoder(file) | ||||
| 	err = encoder.Encode(session) | ||||
|  | ||||
| 	return err | ||||
| 	defer file.Close() | ||||
|  | ||||
| 	encoder := gob.NewEncoder(file) | ||||
|  | ||||
| 	return encoder.Encode(session) | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) restoreSession() (*whatsapp.Session, error) { | ||||
| 	session, err := b.readSession() | ||||
| 	if err != nil { | ||||
| 		b.Log.Warn(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugln("Restoring WhatsApp session..") | ||||
|  | ||||
| 	session, err = b.conn.RestoreWithSession(session) | ||||
| 	if err != nil { | ||||
| 		// restore session connection timed out (I couldn't get over it without logging in again) | ||||
| 		return nil, errors.New("failed to restore session: " + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugln("Session restored successfully!") | ||||
|  | ||||
| 	return &session, nil | ||||
| } | ||||
|  | ||||
| func (b *Bwhatsapp) getSenderName(senderJid string) string { | ||||
| @@ -80,8 +101,32 @@ func (b *Bwhatsapp) getSenderName(senderJid string) string { | ||||
| 		// if user is not in phone contacts | ||||
| 		// it is the most obvious scenario unless you sync your phone contacts with some remote updated source | ||||
| 		// users can change it in their WhatsApp settings -> profile -> click on Avatar | ||||
| 		return sender.Notify | ||||
| 		if sender.Notify != "" { | ||||
| 			return sender.Notify | ||||
| 		} | ||||
|  | ||||
| 		if sender.Short != "" { | ||||
| 			return sender.Short | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// try to reload this contact | ||||
| 	if _, err := b.conn.Contacts(); err != nil { | ||||
| 		b.Log.Errorf("error on update of contacts: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if contact, exists := b.conn.Store.Contacts[senderJid]; exists { | ||||
| 		// Add it to the user map | ||||
| 		b.users[senderJid] = contact | ||||
|  | ||||
| 		if contact.Name != "" { | ||||
| 			return contact.Name | ||||
| 		} | ||||
| 		// if user is not in phone contacts | ||||
| 		// same as above | ||||
| 		return contact.Notify | ||||
| 	} | ||||
|  | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| @@ -89,6 +134,7 @@ func (b *Bwhatsapp) getSenderNotify(senderJid string) string { | ||||
| 	if sender, exists := b.users[senderJid]; exists { | ||||
| 		return sender.Notify | ||||
| 	} | ||||
|  | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| @@ -97,11 +143,20 @@ func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) { | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get avatar: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	content := <-data | ||||
| 	info := &ProfilePicInfo{} | ||||
|  | ||||
| 	err = json.Unmarshal([]byte(content), info) | ||||
| 	if err != nil { | ||||
| 		return info, fmt.Errorf("failed to unmarshal avatar info: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return info, nil | ||||
| } | ||||
|  | ||||
| func isGroupJid(identifier string) bool { | ||||
| 	return strings.HasSuffix(identifier, "@g.us") || | ||||
| 		strings.HasSuffix(identifier, "@temp") || | ||||
| 		strings.HasSuffix(identifier, "@broadcast") | ||||
| } | ||||
|   | ||||
| @@ -28,7 +28,6 @@ const ( | ||||
| type Bwhatsapp struct { | ||||
| 	*bridge.Config | ||||
|  | ||||
| 	// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21 | ||||
| 	session   *whatsapp.Session | ||||
| 	conn      *whatsapp.Conn | ||||
| 	startedAt uint64 | ||||
| @@ -40,6 +39,12 @@ type Bwhatsapp struct { | ||||
| // 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) | ||||
|  | ||||
| 	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") | ||||
| 	} | ||||
| @@ -50,24 +55,19 @@ func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 		users:       make(map[string]whatsapp.Contact), | ||||
| 		userAvatars: make(map[string]string), | ||||
| 	} | ||||
|  | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| // Connect to WhatsApp. Required implementation of the Bridger interface | ||||
| // https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16 | ||||
| func (b *Bwhatsapp) Connect() error { | ||||
| 	b.RLock() // TODO do we need locking for Whatsapp? | ||||
| 	defer b.RUnlock() | ||||
|  | ||||
| 	number := b.GetString(cfgNumber) | ||||
| 	if number == "" { | ||||
| 		return errors.New("WhatsApp's telephone Number need to be configured") | ||||
| 		return errors.New("whatsapp's telephone number need to be configured") | ||||
| 	} | ||||
|  | ||||
| 	// https://github.com/Rhymen/go-whatsapp#creating-a-connection | ||||
| 	b.Log.Debugln("Connecting to WhatsApp..") | ||||
| 	conn, err := whatsapp.NewConn(20 * time.Second) | ||||
| 	conn.SetClientVersion(0, 4, 2080) | ||||
| 	if err != nil { | ||||
| 		return errors.New("failed to connect to WhatsApp: " + err.Error()) | ||||
| 	} | ||||
| @@ -78,35 +78,18 @@ func (b *Bwhatsapp) Connect() error { | ||||
| 	b.Log.Debugln("WhatsApp connection successful") | ||||
|  | ||||
| 	// load existing session in order to keep it between restarts | ||||
| 	if b.session == nil { | ||||
| 		var session whatsapp.Session | ||||
| 		session, err = b.readSession() | ||||
|  | ||||
| 		if err == nil { | ||||
| 			b.Log.Debugln("Restoring WhatsApp session..") | ||||
|  | ||||
| 			// https://github.com/Rhymen/go-whatsapp#restore | ||||
| 			session, err = b.conn.RestoreWithSession(session) | ||||
| 			if err != nil { | ||||
| 				// TODO return or continue to normal login? | ||||
| 				// restore session connection timed out (I couldn't get over it without logging in again) | ||||
| 				return errors.New("failed to restore session: " + err.Error()) | ||||
| 			} | ||||
|  | ||||
| 			b.session = &session | ||||
| 			b.Log.Debugln("Session restored successfully!") | ||||
| 		} else { | ||||
| 			b.Log.Warn(err.Error()) | ||||
| 		} | ||||
| 	b.session, err = b.restoreSession() | ||||
| 	if err != nil { | ||||
| 		b.Log.Warn(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	// login to a new session | ||||
| 	if b.session == nil { | ||||
| 		err = b.Login() | ||||
| 		if err != nil { | ||||
| 		if err = b.Login(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	b.startedAt = uint64(time.Now().Unix()) | ||||
|  | ||||
| 	_, err = b.conn.Contacts() | ||||
| @@ -114,6 +97,13 @@ func (b *Bwhatsapp) Connect() error { | ||||
| 		return fmt.Errorf("error on update of contacts: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013 | ||||
| 	for len(b.conn.Store.Contacts) == 0 { | ||||
| 		b.conn.Contacts() // nolint:errcheck | ||||
|  | ||||
| 		<-time.After(1 * time.Second) | ||||
| 	} | ||||
|  | ||||
| 	// map all the users | ||||
| 	for id, contact := range b.conn.Store.Contacts { | ||||
| 		if !isGroupJid(id) && id != "status@broadcast" { | ||||
| @@ -130,12 +120,13 @@ func (b *Bwhatsapp) Connect() error { | ||||
| 			info, err := b.GetProfilePicThumb(jid) | ||||
| 			if err != nil { | ||||
| 				b.Log.Warnf("Could not get profile photo of %s: %v", jid, err) | ||||
|  | ||||
| 			} else { | ||||
| 				// TODO any race conditions here? | ||||
| 				b.Lock() | ||||
| 				b.userAvatars[jid] = info.URL | ||||
| 				b.Unlock() | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		b.Log.Debug("Finished getting avatars..") | ||||
| 	}() | ||||
|  | ||||
| @@ -152,8 +143,10 @@ func (b *Bwhatsapp) Login() error { | ||||
| 	session, err := b.conn.Login(qrChan) | ||||
| 	if err != nil { | ||||
| 		b.Log.Warnln("Failed to log in:", err) | ||||
|  | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	b.session = &session | ||||
|  | ||||
| 	b.Log.Infof("Logged into session: %#v", session) | ||||
| @@ -164,74 +157,62 @@ func (b *Bwhatsapp) Login() error { | ||||
| 		fmt.Fprintf(os.Stderr, "error saving session: %v\n", err) | ||||
| 	} | ||||
|  | ||||
| 	// TODO change connection strings to configured ones longClientName:"github.com/rhymen/go-whatsapp", shortClientName:"go-whatsapp"}" prefix=whatsapp | ||||
| 	// TODO get also a nice logo | ||||
|  | ||||
| 	// TODO notification about unplugged and dead battery | ||||
| 	// conn.Info: Wid, Pushname, Connected, Battery, Plugged | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Disconnect is called while reconnecting to the bridge | ||||
| // TODO 42wim Documentation would be helpful on when reconnects happen and what should be done in this function | ||||
| // Required implementation of the Bridger interface | ||||
| // https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16 | ||||
| func (b *Bwhatsapp) Disconnect() error { | ||||
| 	// We could Logout, but that would close the session completely and would require a new QR code scan | ||||
| 	// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381 | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func isGroupJid(identifier string) bool { | ||||
| 	return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp") | ||||
| } | ||||
|  | ||||
| // JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name' | ||||
| // Required implementation of the Bridger interface | ||||
| // https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16 | ||||
| func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error { | ||||
| 	byJid := isGroupJid(channel.Name) | ||||
|  | ||||
| 	// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013 | ||||
| 	for len(b.conn.Store.Contacts) == 0 { | ||||
| 		b.conn.Contacts() // nolint:errcheck | ||||
| 		<-time.After(1 * time.Second) | ||||
| 	} | ||||
|  | ||||
| 	// verify if we are member of the given group | ||||
| 	if byJid { | ||||
| 		// channel.Name specifies static group jID, not the name | ||||
| 		if _, exists := b.conn.Store.Contacts[channel.Name]; !exists { | ||||
| 			return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name) | ||||
| 		} | ||||
| 	} else { | ||||
| 		// channel.Name specifies group name that might change, warn about it | ||||
| 		var jids []string | ||||
| 		for id, contact := range b.conn.Store.Contacts { | ||||
| 			if isGroupJid(id) && contact.Name == channel.Name { | ||||
| 				jids = append(jids, id) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		switch len(jids) { | ||||
| 		case 0: | ||||
| 			// didn't match any group - print out possibilites | ||||
| 			// TODO sort | ||||
| 			// copy b; | ||||
| 			//sort.Slice(people, func(i, j int) bool { | ||||
| 			//	return people[i].Age > people[j].Age | ||||
| 			//}) | ||||
| 			for id, contact := range b.conn.Store.Contacts { | ||||
| 				if isGroupJid(id) { | ||||
| 					b.Log.Infof("%s %s", contact.Jid, contact.Name) | ||||
| 				} | ||||
| 			} | ||||
| 			return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 		case 1: | ||||
| 			return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name) | ||||
|  | ||||
| 		default: | ||||
| 			return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids) | ||||
| 	// channel.Name specifies group name that might change, warn about it | ||||
| 	var jids []string | ||||
| 	for id, contact := range b.conn.Store.Contacts { | ||||
| 		if isGroupJid(id) && contact.Name == channel.Name { | ||||
| 			jids = append(jids, id) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| 	switch len(jids) { | ||||
| 	case 0: | ||||
| 		// didn't match any group - print out possibilites | ||||
| 		for id, contact := range b.conn.Store.Contacts { | ||||
| 			if isGroupJid(id) { | ||||
| 				b.Log.Infof("%s %s", contact.Jid, contact.Name) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name) | ||||
| 	case 1: | ||||
| 		return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name) | ||||
| 	default: | ||||
| 		return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Post a document message from the bridge to WhatsApp | ||||
| @@ -305,22 +286,23 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) { | ||||
| 		if msg.ID == "" { | ||||
| 			// No message ID in case action is executed on a message sent before the bridge was started | ||||
| 			// and then the bridge cache doesn't have this message ID mapped | ||||
|  | ||||
| 			// TODO 42wim Doesn't the app get clogged with a ton of IDs after some time of running? | ||||
| 			// WhatsApp allows to set any ID so in that case we could use external IDs and don't do mapping | ||||
| 			// but external IDs are not set | ||||
| 			return "", nil | ||||
| 		} | ||||
| 		// TODO delete message on WhatsApp https://github.com/Rhymen/go-whatsapp/issues/100 | ||||
| 		return "", nil | ||||
|  | ||||
| 		_, err := b.conn.RevokeMessage(msg.Channel, msg.ID, true) | ||||
|  | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	// Edit message | ||||
| 	if msg.ID != "" { | ||||
| 		b.Log.Debugf("updating message with id %s", msg.ID) | ||||
|  | ||||
| 		msg.Text += " (edited)" | ||||
| 		// TODO handle edit as a message reply with updated text | ||||
| 		if b.GetString("editsuffix") != "" { | ||||
| 			msg.Text += b.GetString("EditSuffix") | ||||
| 		} else { | ||||
| 			msg.Text += " (edited)" | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Handle Upload a file | ||||
| @@ -350,16 +332,7 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) { | ||||
|  | ||||
| 	b.Log.Debugf("=> Sending %#v", msg) | ||||
|  | ||||
| 	// create message ID | ||||
| 	// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented | ||||
| 	idBytes := make([]byte, 10) | ||||
| 	if _, err := rand.Read(idBytes); err != nil { | ||||
| 		b.Log.Warn(err.Error()) | ||||
| 	} | ||||
| 	message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes)) | ||||
| 	_, err := b.conn.Send(message) | ||||
|  | ||||
| 	return message.Info.Id, err | ||||
| 	return b.conn.Send(message) | ||||
| } | ||||
|  | ||||
| // TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76 | ||||
|   | ||||
							
								
								
									
										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 | ||||
| } | ||||
							
								
								
									
										34
									
								
								bridge/xmpp/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | ||||
| package bxmpp | ||||
|  | ||||
| import ( | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| 	"github.com/42wim/matterbridge/bridge/helper" | ||||
| 	"github.com/matterbridge/go-xmpp" | ||||
| ) | ||||
|  | ||||
| // handleDownloadAvatar downloads the avatar of userid from channel | ||||
| // sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. | ||||
| // logs an error message if it fails | ||||
| func (b *Bxmpp) handleDownloadAvatar(avatar xmpp.AvatarData) { | ||||
| 	rmsg := config.Message{ | ||||
| 		Username: "system", | ||||
| 		Text:     "avatar", | ||||
| 		Channel:  b.parseChannel(avatar.From), | ||||
| 		Account:  b.Account, | ||||
| 		UserID:   avatar.From, | ||||
| 		Event:    config.EventAvatarDownload, | ||||
| 		Extra:    make(map[string][]interface{}), | ||||
| 	} | ||||
| 	if _, ok := b.avatarMap[avatar.From]; !ok { | ||||
| 		b.Log.Debugf("Avatar.From: %s", avatar.From) | ||||
|  | ||||
| 		err := helper.HandleDownloadSize(b.Log, &rmsg, avatar.From+".png", int64(len(avatar.Data)), b.General) | ||||
| 		if err != nil { | ||||
| 			b.Log.Error(err) | ||||
| 			return | ||||
| 		} | ||||
| 		helper.HandleDownloadData(b.Log, &rmsg, avatar.From+".png", rmsg.Text, "", &avatar.Data, b.General) | ||||
| 		b.Log.Debugf("Avatar download complete") | ||||
| 		b.Remote <- rmsg | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										30
									
								
								bridge/xmpp/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| package bxmpp | ||||
|  | ||||
| import ( | ||||
| 	"regexp" | ||||
|  | ||||
| 	"github.com/42wim/matterbridge/bridge/config" | ||||
| ) | ||||
|  | ||||
| var pathRegex = regexp.MustCompile("[^a-zA-Z0-9]+") | ||||
|  | ||||
| // GetAvatar constructs a URL for a given user-avatar if it is available in the cache. | ||||
| func getAvatar(av map[string]string, userid string, general *config.Protocol) string { | ||||
| 	if hash, ok := av[userid]; ok { | ||||
| 		// NOTE: This does not happen in bridge/helper/helper.go but messes up XMPP | ||||
| 		id := pathRegex.ReplaceAllString(userid, "_") | ||||
| 		return general.MediaServerDownload + "/" + hash + "/" + id + ".png" | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (b *Bxmpp) cacheAvatar(msg *config.Message) string { | ||||
| 	fi := msg.Extra["file"][0].(config.FileInfo) | ||||
| 	/* if we have a sha we have successfully uploaded the file to the media server, | ||||
| 	so we can now cache the sha */ | ||||
| 	if fi.SHA != "" { | ||||
| 		b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID) | ||||
| 		b.avatarMap[msg.UserID] = fi.SHA | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| @@ -1,8 +1,12 @@ | ||||
| package bxmpp | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/tls" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| @@ -23,12 +27,17 @@ type Bxmpp struct { | ||||
| 	xmppMap   map[string]string | ||||
| 	connected bool | ||||
| 	sync.RWMutex | ||||
|  | ||||
| 	avatarAvailability map[string]bool | ||||
| 	avatarMap          map[string]string | ||||
| } | ||||
|  | ||||
| func New(cfg *bridge.Config) bridge.Bridger { | ||||
| 	return &Bxmpp{ | ||||
| 		Config:  cfg, | ||||
| 		xmppMap: make(map[string]string), | ||||
| 		Config:             cfg, | ||||
| 		xmppMap:            make(map[string]string), | ||||
| 		avatarAvailability: make(map[string]bool), | ||||
| 		avatarMap:          make(map[string]string), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -67,17 +76,35 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { | ||||
| 	if msg.Event == config.EventMsgDelete { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	b.Log.Debugf("=> Receiving %#v", msg) | ||||
|  | ||||
| 	if msg.Event == config.EventAvatarDownload { | ||||
| 		return b.cacheAvatar(&msg), nil | ||||
| 	} | ||||
|  | ||||
| 	// Make a action /me of the message, prepend the username with it. | ||||
| 	// https://xmpp.org/extensions/xep-0245.html | ||||
| 	if msg.Event == config.EventUserAction { | ||||
| 		msg.Username = "/me " + msg.Username | ||||
| 	} | ||||
|  | ||||
| 	// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support). | ||||
| 	var err error | ||||
| 	if msg.Extra != nil { | ||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | ||||
| 			b.Log.Debugf("=> Sending attachement message %#v", rmsg) | ||||
| 			if _, err := b.xc.Send(xmpp.Chat{ | ||||
| 				Type:   "groupchat", | ||||
| 				Remote: rmsg.Channel + "@" + b.GetString("Muc"), | ||||
| 				Text:   rmsg.Username + rmsg.Text, | ||||
| 			}); err != nil { | ||||
| 			if b.GetString("WebhookURL") != "" { | ||||
| 				err = b.postSlackCompatibleWebhook(msg) | ||||
| 			} else { | ||||
| 				_, err = b.xc.Send(xmpp.Chat{ | ||||
| 					Type:   "groupchat", | ||||
| 					Remote: rmsg.Channel + "@" + b.GetString("Muc"), | ||||
| 					Text:   rmsg.Username + rmsg.Text, | ||||
| 				}) | ||||
| 			} | ||||
|  | ||||
| 			if err != nil { | ||||
| 				b.Log.WithError(err).Error("Unable to send message with share URL.") | ||||
| 			} | ||||
| 		} | ||||
| @@ -86,13 +113,23 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if b.GetString("WebhookURL") != "" { | ||||
| 		b.Log.Debugf("Sending message using Webhook") | ||||
| 		err := b.postSlackCompatibleWebhook(msg) | ||||
| 		if err != nil { | ||||
| 			b.Log.Errorf("Failed to send message using webhook: %s", err) | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	// Post normal message. | ||||
| 	var msgReplaceID string | ||||
| 	msgID := xid.New().String() | ||||
| 	if msg.ID != "" { | ||||
| 		msgID = msg.ID | ||||
| 		msgReplaceID = msg.ID | ||||
| 	} | ||||
| 	// Post normal message. | ||||
| 	b.Log.Debugf("=> Sending message %#v", msg) | ||||
| 	if _, err := b.xc.Send(xmpp.Chat{ | ||||
| 		Type:      "groupchat", | ||||
| @@ -106,12 +143,46 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { | ||||
| 	return msgID, 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")) | ||||
| func (b *Bxmpp) postSlackCompatibleWebhook(msg config.Message) error { | ||||
| 	type XMPPWebhook struct { | ||||
| 		Username string `json:"username"` | ||||
| 		Text     string `json:"text"` | ||||
| 	} | ||||
| 	webhookBody, err := json.Marshal(XMPPWebhook{ | ||||
| 		Username: msg.Username, | ||||
| 		Text:     msg.Text, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Failed to marshal webhook: %s", err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	resp, err := http.Post(b.GetString("WebhookURL")+"/"+url.QueryEscape(msg.Channel), "application/json", bytes.NewReader(webhookBody)) | ||||
| 	if err != nil { | ||||
| 		b.Log.Errorf("Failed to POST webhook: %s", err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	resp.Body.Close() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (b *Bxmpp) createXMPP() error { | ||||
| 	var serverName string | ||||
| 	switch { | ||||
| 	case !b.GetBool("Anonymous"): | ||||
| 		if !strings.Contains(b.GetString("Jid"), "@") { | ||||
| 			return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid")) | ||||
| 		} | ||||
| 		serverName = strings.Split(b.GetString("Jid"), "@")[1] | ||||
| 	case !strings.Contains(b.GetString("Server"), ":"): | ||||
| 		serverName = strings.Split(b.GetString("Server"), ":")[0] | ||||
| 	default: | ||||
| 		serverName = b.GetString("Server") | ||||
| 	} | ||||
|  | ||||
| 	tc := &tls.Config{ | ||||
| 		ServerName:         strings.Split(b.GetString("Jid"), "@")[1], | ||||
| 		ServerName:         serverName, | ||||
| 		InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec | ||||
| 	} | ||||
|  | ||||
| @@ -122,14 +193,14 @@ func (b *Bxmpp) createXMPP() error { | ||||
| 		User:                         b.GetString("Jid"), | ||||
| 		Password:                     b.GetString("Password"), | ||||
| 		NoTLS:                        true, | ||||
| 		StartTLS:                     true, | ||||
| 		StartTLS:                     !b.GetBool("NoTLS"), | ||||
| 		TLSConfig:                    tc, | ||||
| 		Debug:                        b.GetBool("debug"), | ||||
| 		Session:                      true, | ||||
| 		Status:                       "", | ||||
| 		StatusMessage:                "", | ||||
| 		Resource:                     "", | ||||
| 		InsecureAllowUnencryptedAuth: false, | ||||
| 		InsecureAllowUnencryptedAuth: b.GetBool("NoTLS"), | ||||
| 	} | ||||
| 	var err error | ||||
| 	b.xc, err = options.NewClient() | ||||
| @@ -212,7 +283,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) { | ||||
| @@ -230,6 +307,16 @@ func (b *Bxmpp) handleXMPP() error { | ||||
| 					event = config.EventTopicChange | ||||
| 				} | ||||
|  | ||||
| 				available, sok := b.avatarAvailability[v.Remote] | ||||
| 				avatar := "" | ||||
| 				if !sok { | ||||
| 					b.Log.Debugf("Requesting avatar data") | ||||
| 					b.avatarAvailability[v.Remote] = false | ||||
| 					b.xc.AvatarRequestData(v.Remote) | ||||
| 				} else if available { | ||||
| 					avatar = getAvatar(b.avatarMap, v.Remote, b.General) | ||||
| 				} | ||||
|  | ||||
| 				msgID := v.ID | ||||
| 				if v.ReplaceID != "" { | ||||
| 					msgID = v.ReplaceID | ||||
| @@ -239,6 +326,7 @@ func (b *Bxmpp) handleXMPP() error { | ||||
| 					Text:     v.Text, | ||||
| 					Channel:  b.parseChannel(v.Remote), | ||||
| 					Account:  b.Account, | ||||
| 					Avatar:   avatar, | ||||
| 					UserID:   v.Remote, | ||||
| 					ID:       msgID, | ||||
| 					Event:    event, | ||||
| @@ -255,6 +343,10 @@ func (b *Bxmpp) handleXMPP() error { | ||||
| 				b.Log.Debugf("<= Message is %#v", rmsg) | ||||
| 				b.Remote <- rmsg | ||||
| 			} | ||||
| 		case xmpp.AvatarData: | ||||
| 			b.handleDownloadAvatar(v) | ||||
| 			b.avatarAvailability[v.From] = true | ||||
| 			b.Log.Debugf("Avatar for %s is now available", v.From) | ||||
| 		case xmpp.Presence: | ||||
| 			// Do nothing. | ||||
| 		} | ||||
| @@ -308,7 +400,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 | ||||
| @@ -347,6 +439,11 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	// Ignore messages posted by our webhook | ||||
| 	if b.GetString("WebhookURL") != "" && strings.Contains(message.ID, "webhookbot") { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	// skip delayed messages | ||||
| 	return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5 | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
| @@ -135,19 +139,25 @@ func (b *Bzulip) handleQueue() error { | ||||
| 			if m.SenderEmail == b.GetString("login") { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			avatarURL := m.AvatarURL | ||||
| 			if !strings.HasPrefix(avatarURL, "http") { | ||||
| 				avatarURL = b.GetString("server") + avatarURL | ||||
| 			} | ||||
|  | ||||
| 			rmsg := config.Message{ | ||||
| 				Username: m.SenderFullName, | ||||
| 				Text:     m.Content, | ||||
| 				Channel:  b.getChannel(m.StreamID) + "/topic:" + m.Subject, | ||||
| 				Account:  b.Account, | ||||
| 				UserID:   strconv.Itoa(m.SenderID), | ||||
| 				Avatar:   m.AvatarURL, | ||||
| 				Avatar:   avatarURL, | ||||
| 			} | ||||
| 			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) | ||||
| 			b.Log.Debugf("<= Message is %#v", rmsg) | ||||
| 			b.Remote <- rmsg | ||||
| 			b.q.LastEventID = m.ID | ||||
| 		} | ||||
|  | ||||
| 		time.Sleep(time.Second * 3) | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										1706
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						| @@ -1,30 +0,0 @@ | ||||
| #!/usr/bin/env bash | ||||
| set -u -e -x -o pipefail | ||||
|  | ||||
| go version | grep go1.14 || exit | ||||
|  | ||||
| VERSION=$(git describe --tags) | ||||
| mkdir ci/binaries | ||||
| GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe | ||||
| GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-amd64 | ||||
| GOOS=linux GOARCH=arm go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-arm | ||||
| GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-darwin-amd64 | ||||
| cd ci | ||||
| cat > deploy.json <<EOF | ||||
| { | ||||
|     "package": { | ||||
|         "name": "Matterbridge", | ||||
|         "repo": "nightly", | ||||
|         "subject": "42wim" | ||||
|     }, | ||||
|     "version": { | ||||
|         "name": "$VERSION" | ||||
|     }, | ||||
|     "files": | ||||
|         [ | ||||
|         {"includePattern": "ci/binaries/(.*)", "uploadPattern":"\$1"} | ||||
|         ], | ||||
|     "publish": true | ||||
| } | ||||
| EOF | ||||
|  | ||||
							
								
								
									
										17
									
								
								ci/lint.sh
									
									
									
									
									
								
							
							
						
						| @@ -1,17 +0,0 @@ | ||||
| #!/usr/bin/env bash | ||||
| set -u -e -x -o pipefail | ||||
|  | ||||
| if [[ -n "${GOLANGCI_VERSION-}" ]]; then | ||||
|   # Retrieve the golangci-lint linter binary. | ||||
|   curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION} | ||||
| fi | ||||
|  | ||||
| # Run the linter. | ||||
| golangci-lint run | ||||
|  | ||||
| # if [[ "${GO111MODULE-off}" == "on" ]]; then | ||||
| #   # If Go modules are active then check that dependencies are correctly maintained. | ||||
| #   go mod tidy | ||||
| #   go mod vendor | ||||
| #   git diff --exit-code --quiet || (echo "Please run 'go mod tidy' to clean up the 'go.mod' and 'go.sum' files."; false) | ||||
| # fi | ||||
							
								
								
									
										17
									
								
								ci/test.sh
									
									
									
									
									
								
							
							
						
						| @@ -1,17 +0,0 @@ | ||||
| #!/usr/bin/env bash | ||||
| set -u -e -x -o pipefail | ||||
|  | ||||
| if [[ -n "${REPORT_COVERAGE+cover}" ]]; then | ||||
|   # Retrieve and prepare CodeClimate's test coverage reporter. | ||||
|   curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter | ||||
|   chmod +x ./cc-test-reporter | ||||
|   ./cc-test-reporter before-build | ||||
| fi | ||||
|  | ||||
| # Run all the tests with the race detector and generate coverage. | ||||
| go test -v -race -coverprofile c.out ./... | ||||
|  | ||||
| if [[ -n "${REPORT_COVERAGE+cover}" && "${TRAVIS_SECURE_ENV_VARS}" == "true" ]]; then | ||||
|   # Upload test coverage to CodeClimate. | ||||
|   ./cc-test-reporter after-build | ||||
| fi | ||||
							
								
								
									
										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 | ||||
| } | ||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bmumble.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| // +build !nomumble | ||||
|  | ||||
| package bridgemap | ||||
|  | ||||
| import ( | ||||
| 	bmumble "github.com/42wim/matterbridge/bridge/mumble" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	FullMap["mumble"] = bmumble.New | ||||
| } | ||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bnctalk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| // +build !nonctalk | ||||
|  | ||||
| package bridgemap | ||||
|  | ||||
| import ( | ||||
| 	btalk "github.com/42wim/matterbridge/bridge/nctalk" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	FullMap["nctalk"] = btalk.New | ||||
| } | ||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bvk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| // +build !novk | ||||
|  | ||||
| package bridgemap | ||||
|  | ||||
| import ( | ||||
| 	bvk "github.com/42wim/matterbridge/bridge/vk" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	FullMap["vk"] = bvk.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 | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| package gateway | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
| @@ -13,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" | ||||
| ) | ||||
|  | ||||
| @@ -65,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. | ||||
| @@ -74,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) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| @@ -108,7 +109,7 @@ func (gw *Gateway) AddBridge(cfg *config.Bridge) error { | ||||
| func (gw *Gateway) checkConfig(cfg *config.Bridge) { | ||||
| 	match := false | ||||
| 	for _, key := range gw.Router.Config.Viper().AllKeys() { | ||||
| 		if strings.HasPrefix(key, cfg.Account) { | ||||
| 		if strings.HasPrefix(key, strings.ToLower(cfg.Account)) { | ||||
| 			match = true | ||||
| 			break | ||||
| 		} | ||||
| @@ -126,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 | ||||
| @@ -298,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]+") | ||||
| @@ -336,20 +354,21 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri | ||||
| 			} | ||||
| 			i++ | ||||
| 		} | ||||
| 		nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1) | ||||
| 		nick = strings.ReplaceAll(nick, "{NOPINGNICK}", msg.Username[:i]+"\u200b"+msg.Username[i:]) | ||||
| 	} | ||||
|  | ||||
| 	nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1) | ||||
| 	nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1) | ||||
| 	nick = strings.Replace(nick, "{GATEWAY}", gw.Name, -1) | ||||
| 	nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1) | ||||
| 	nick = strings.Replace(nick, "{NICK}", msg.Username, -1) | ||||
| 	nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1) | ||||
| 	nick = strings.ReplaceAll(nick, "{BRIDGE}", br.Name) | ||||
| 	nick = strings.ReplaceAll(nick, "{PROTOCOL}", br.Protocol) | ||||
| 	nick = strings.ReplaceAll(nick, "{GATEWAY}", gw.Name) | ||||
| 	nick = strings.ReplaceAll(nick, "{LABEL}", br.GetString("Label")) | ||||
| 	nick = strings.ReplaceAll(nick, "{NICK}", msg.Username) | ||||
| 	nick = strings.ReplaceAll(nick, "{USERID}", msg.UserID) | ||||
| 	nick = strings.ReplaceAll(nick, "{CHANNEL}", msg.Channel) | ||||
| 	tengoNick, err := gw.modifyUsernameTengo(msg, br) | ||||
| 	if err != nil { | ||||
| 		gw.logger.Errorf("modifyUsernameTengo error: %s", err) | ||||
| 	} | ||||
| 	nick = strings.Replace(nick, "{TENGO}", tengoNick, -1) //nolint:gocritic | ||||
| 	nick = strings.ReplaceAll(nick, "{TENGO}", tengoNick) | ||||
| 	return nick | ||||
| } | ||||
|  | ||||
| @@ -363,14 +382,28 @@ func (gw *Gateway) modifyAvatar(msg *config.Message, dest *bridge.Bridge) string | ||||
| } | ||||
|  | ||||
| func (gw *Gateway) modifyMessage(msg *config.Message) { | ||||
| 	if err := modifyMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil { | ||||
| 	if gw.BridgeValues().General.TengoModifyMessage != "" { | ||||
| 		gw.logger.Warnf("General TengoModifyMessage=%s is deprecated and will be removed in v1.20.0, please move to Tengo InMessage=%s", gw.BridgeValues().General.TengoModifyMessage, gw.BridgeValues().General.TengoModifyMessage) | ||||
| 	} | ||||
|  | ||||
| 	if err := modifyInMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil { | ||||
| 		gw.logger.Errorf("TengoModifyMessage failed: %s", err) | ||||
| 	} | ||||
| 	if err := modifyMessageTengo(gw.BridgeValues().Tengo.Message, msg); err != nil { | ||||
|  | ||||
| 	inMessage := gw.BridgeValues().Tengo.InMessage | ||||
| 	if inMessage == "" { | ||||
| 		inMessage = gw.BridgeValues().Tengo.Message | ||||
| 		if inMessage != "" { | ||||
| 			gw.logger.Warnf("Tengo Message=%s is deprecated and will be removed in v1.20.0, please move to Tengo InMessage=%s", inMessage, inMessage) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := modifyInMessageTengo(inMessage, msg); err != nil { | ||||
| 		gw.logger.Errorf("Tengo.Message failed: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	// replace :emoji: to unicode | ||||
| 	emoji.ReplacePadding = "" | ||||
| 	msg.Text = emoji.Sprint(msg.Text) | ||||
|  | ||||
| 	br := gw.Bridges[msg.Account] | ||||
| @@ -416,44 +449,65 @@ func (gw *Gateway) SendMessage( | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Only send irc notices to irc | ||||
| 	if msg.Event == config.EventNoticeIRC && dest.Protocol != "irc" { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	// Too noisy to log like other events | ||||
| 	debugSendMessage := "" | ||||
| 	if msg.Event != config.EventUserTyping { | ||||
| 		gw.logger.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name) | ||||
| 		debugSendMessage = fmt.Sprintf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name) | ||||
| 	} | ||||
|  | ||||
| 	msg.Channel = channel.Name | ||||
| 	msg.Avatar = gw.modifyAvatar(rmsg, dest) | ||||
| 	msg.Username = gw.modifyUsername(rmsg, dest) | ||||
|  | ||||
| 	msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel) | ||||
| 	// 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 | ||||
| 	// this means that we didn't find it in the cache so set it "msg-parent-not-found" | ||||
| 	// this means that we didn't find it in the cache so set it to a "msg-parent-not-found" constant | ||||
| 	if msg.ParentID == "" && rmsg.ParentID != "" { | ||||
| 		msg.ParentID = "msg-parent-not-found" | ||||
| 		msg.ParentID = config.ParentIDNotFound | ||||
| 	} | ||||
|  | ||||
| 	err := gw.modifySendMessageTengo(rmsg, &msg, dest) | ||||
| 	drop, err := gw.modifyOutMessageTengo(rmsg, &msg, dest) | ||||
| 	if err != nil { | ||||
| 		gw.logger.Errorf("modifySendMessageTengo: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	if drop { | ||||
| 		gw.logger.Debugf("=> Tengo dropping %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name) | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	if debugSendMessage != "" { | ||||
| 		gw.logger.Debug(debugSendMessage) | ||||
| 	} | ||||
| 	// if we are using mattermost plugin account, send messages to MattermostPlugin channel | ||||
| 	// that can be picked up by the mattermost matterbridge plugin | ||||
| 	if dest.Account == "mattermost.plugin" { | ||||
| 		gw.Router.MattermostPlugin <- msg | ||||
| 	} | ||||
|  | ||||
| 	defer func(t time.Time) { | ||||
| 		gw.logger.Debugf("=> Send from %s (%s) to %s (%s) took %s", msg.Account, rmsg.Channel, dest.Account, channel.Name, time.Since(t)) | ||||
| 	}(time.Now()) | ||||
|  | ||||
| 	mID, err := dest.Send(msg) | ||||
| 	if err != nil { | ||||
| 		return mID, err | ||||
| @@ -463,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 | ||||
| } | ||||
| @@ -505,7 +559,7 @@ func getProtocol(msg *config.Message) string { | ||||
| 	return p[0] | ||||
| } | ||||
|  | ||||
| func modifyMessageTengo(filename string, msg *config.Message) error { | ||||
| func modifyInMessageTengo(filename string, msg *config.Message) error { | ||||
| 	if filename == "" { | ||||
| 		return nil | ||||
| 	} | ||||
| @@ -517,6 +571,7 @@ func modifyMessageTengo(filename string, msg *config.Message) error { | ||||
| 	s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) | ||||
| 	_ = s.Add("msgText", msg.Text) | ||||
| 	_ = s.Add("msgUsername", msg.Username) | ||||
| 	_ = s.Add("msgUserID", msg.UserID) | ||||
| 	_ = s.Add("msgAccount", msg.Account) | ||||
| 	_ = s.Add("msgChannel", msg.Channel) | ||||
| 	c, err := s.Compile() | ||||
| @@ -545,6 +600,7 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) ( | ||||
| 	_ = s.Add("result", "") | ||||
| 	_ = s.Add("msgText", msg.Text) | ||||
| 	_ = s.Add("msgUsername", msg.Username) | ||||
| 	_ = s.Add("msgUserID", msg.UserID) | ||||
| 	_ = s.Add("nick", msg.Username) | ||||
| 	_ = s.Add("msgAccount", msg.Account) | ||||
| 	_ = s.Add("msgChannel", msg.Channel) | ||||
| @@ -564,22 +620,28 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) ( | ||||
| 	return c.Get("result").String(), nil | ||||
| } | ||||
|  | ||||
| func (gw *Gateway) modifySendMessageTengo(origmsg *config.Message, msg *config.Message, br *bridge.Bridge) error { | ||||
| func (gw *Gateway) modifyOutMessageTengo(origmsg *config.Message, msg *config.Message, br *bridge.Bridge) (bool, error) { | ||||
| 	filename := gw.BridgeValues().Tengo.OutMessage | ||||
| 	var res []byte | ||||
| 	var err error | ||||
| 	var ( | ||||
| 		res  []byte | ||||
| 		err  error | ||||
| 		drop bool | ||||
| 	) | ||||
|  | ||||
| 	if filename == "" { | ||||
| 		res, err = internal.Asset("tengo/outmessage.tengo") | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return drop, err | ||||
| 		} | ||||
| 	} else { | ||||
| 		res, err = ioutil.ReadFile(filename) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return drop, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	s := tengo.NewScript(res) | ||||
|  | ||||
| 	s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) | ||||
| 	_ = s.Add("inAccount", origmsg.Account) | ||||
| 	_ = s.Add("inProtocol", origmsg.Protocol) | ||||
| @@ -593,14 +655,20 @@ func (gw *Gateway) modifySendMessageTengo(origmsg *config.Message, msg *config.M | ||||
| 	_ = s.Add("outEvent", msg.Event) | ||||
| 	_ = s.Add("msgText", msg.Text) | ||||
| 	_ = s.Add("msgUsername", msg.Username) | ||||
| 	_ = s.Add("msgUserID", msg.UserID) | ||||
| 	_ = s.Add("msgDrop", drop) | ||||
| 	c, err := s.Compile() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return drop, err | ||||
| 	} | ||||
|  | ||||
| 	if err := c.Run(); err != nil { | ||||
| 		return err | ||||
| 		return drop, err | ||||
| 	} | ||||
|  | ||||
| 	drop = c.Get("msgDrop").Bool() | ||||
| 	msg.Text = c.Get("msgText").String() | ||||
| 	msg.Username = c.Get("msgUsername").String() | ||||
| 	return nil | ||||
|  | ||||
| 	return drop, 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() { | ||||
| @@ -533,7 +501,7 @@ func (s *ignoreTestSuite) TestIgnoreNicks() { | ||||
| func BenchmarkTengo(b *testing.B) { | ||||
| 	msg := &config.Message{Username: "user", Text: "blah testing", Account: "protocol.account", Channel: "mychannel"} | ||||
| 	for n := 0; n < b.N; n++ { | ||||
| 		err := modifyMessageTengo("bench.tengo", msg) | ||||
| 		err := modifyInMessageTengo("bench.tengo", msg) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|   | ||||
| @@ -169,7 +169,7 @@ func (gw *Gateway) ignoreEvent(event string, dest *bridge.Bridge) bool { | ||||
| 	switch event { | ||||
| 	case config.EventAvatarDownload: | ||||
| 		// Avatar downloads are only relevant for telegram and mattermost for now | ||||
| 		if dest.Protocol != "mattermost" && dest.Protocol != "telegram" { | ||||
| 		if dest.Protocol != "mattermost" && dest.Protocol != "telegram" && dest.Protocol != "xmpp" { | ||||
| 			return true | ||||
| 		} | ||||
| 	case config.EventJoinLeave: | ||||
| @@ -179,7 +179,7 @@ func (gw *Gateway) ignoreEvent(event string, dest *bridge.Bridge) bool { | ||||
| 		} | ||||
| 	case config.EventTopicChange: | ||||
| 		// only relay topic change when used in some way on other side | ||||
| 		if dest.GetBool("ShowTopicChange") && dest.GetBool("SyncTopic") { | ||||
| 		if !dest.GetBool("ShowTopicChange") && !dest.GetBool("SyncTopic") { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -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 | ||||
| @@ -160,7 +162,7 @@ func (r *Router) handleReceive() { | ||||
| 				// For some bridges we always add/update the message ID. | ||||
| 				// This is necessary as msgIDs will change if a bridge returns | ||||
| 				// a different ID in response to edits. | ||||
| 				if !exists || msg.Protocol == "discord" { | ||||
| 				if !exists { | ||||
| 					gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs) | ||||
| 				} | ||||
| 			} | ||||
|   | ||||
							
								
								
									
										199
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						| @@ -1,64 +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/Jeffail/gabs v1.1.1 // indirect | ||||
| 	github.com/Philipp15b/go-steam v1.0.1-0.20190816133340-b04c5a83c1c0 | ||||
| 	github.com/Rhymen/go-whatsapp v0.1.1-0.20200408093540-2f227c53b44f | ||||
| 	github.com/d5/tengo/v2 v2.1.2 | ||||
| 	github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec | ||||
| 	github.com/fsnotify/fsnotify v1.4.7 | ||||
| 	github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible | ||||
| 	github.com/gomarkdown/markdown v0.0.0-20200127000047-1813ea067497 | ||||
| 	github.com/google/gops v0.3.6 | ||||
| 	github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect | ||||
| 	github.com/gorilla/schema v1.1.0 | ||||
| 	github.com/gorilla/websocket v1.4.1 | ||||
| 	github.com/hashicorp/golang-lru v0.5.3 | ||||
| 	github.com/hpcloud/tail v1.0.0 // indirect | ||||
| 	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.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-20200226211841-4e48f3eaef3e | ||||
| 	github.com/labstack/echo/v4 v4.1.13 | ||||
| 	github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 | ||||
| 	github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d | ||||
| 	github.com/matterbridge/discordgo v0.18.1-0.20200308151012-aa40f01cbcc3 | ||||
| 	github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible | ||||
| 	github.com/matterbridge/go-xmpp v0.0.0-20200329150250-5812999b292b | ||||
| 	github.com/matterbridge/gomatrix v0.0.0-20200209224845-c2104d7936a6 | ||||
| 	github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18 | ||||
| 	github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61 | ||||
| 	github.com/mattermost/mattermost-server v5.5.0+incompatible | ||||
| 	github.com/mattn/go-runewidth v0.0.7 // indirect | ||||
| 	github.com/mattn/godown v0.0.0-20180312012330-2e9e17e0ea51 | ||||
| 	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect | ||||
| 	github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect | ||||
| 	github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect | ||||
| 	github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 | ||||
| 	github.com/nicksnyder/go-i18n v1.4.0 // indirect | ||||
| 	github.com/onsi/ginkgo v1.6.0 // indirect | ||||
| 	github.com/onsi/gomega v1.4.1 // indirect | ||||
| 	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/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/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/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect | ||||
| 	github.com/rs/xid v1.2.1 | ||||
| 	github.com/russross/blackfriday v1.5.2 | ||||
| 	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca | ||||
| 	github.com/shazow/ssh-chat v1.8.3-0.20200308224626-80ddf1f43a98 | ||||
| 	github.com/sirupsen/logrus v1.4.2 | ||||
| 	github.com/slack-go/slack v0.6.3 | ||||
| 	github.com/spf13/viper v1.6.1 | ||||
| 	github.com/stretchr/testify v1.4.0 | ||||
| 	github.com/technoweenie/multipartstreamer v1.0.1 // indirect | ||||
| 	github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect | ||||
| 	github.com/yaegashi/msgraph.go v0.1.2 | ||||
| 	github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447 | ||||
| 	golang.org/x/image v0.0.0-20191214001246-9130b4cfad52 | ||||
| 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d | ||||
| 	gopkg.in/fsnotify.v1 v1.4.7 // indirect | ||||
| 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect | ||||
| 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect | ||||
| 	github.com/rs/xid v1.5.0 | ||||
| 	github.com/russross/blackfriday v1.6.0 | ||||
| 	github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d | ||||
| 	github.com/shazow/ssh-chat v1.10.1 | ||||
| 	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/yaegashi/msgraph.go v0.1.4 | ||||
| 	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 | ||||
| ) | ||||
|  | ||||
| //replace github.com/bwmarrin/discordgo v0.20.2 => github.com/matterbridge/discordgo v0.18.1-0.20200109173909-ed873362fa43 | ||||
| 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 | ||||
| ) | ||||
|  | ||||
| go 1.13 | ||||
| //replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419 | ||||
|  | ||||
| go 1.22.0 | ||||
|   | ||||
							
								
								
									
										806
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						| @@ -1,347 +1,653 @@ | ||||
| cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||
| cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||
| cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||
| github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 h1:IZtuWGfzQnKnCSu+vl8WGLhpVQ5Uvy3rlSwqXSg+sQg= | ||||
| github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557/go.mod h1:jL0YSXMs/txjtGJ4PWrmETOk6KUHMDPMshgQZlTeB3Y= | ||||
| cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= | ||||
| dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= | ||||
| dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= | ||||
| dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= | ||||
| dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= | ||||
| filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= | ||||
| filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= | ||||
| git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= | ||||
| github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII= | ||||
| github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk= | ||||
| github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= | ||||
| github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989 h1:+wrfJITuBoQOE6ST4k3c4EortNVQXVhfAbwt0M/j0+Y= | ||||
| github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989/go.mod h1:aDWSWjsayFyGTvHZH3v4ijGXEBe51xcEkAK+NUWeOeo= | ||||
| github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f h1:aUkwZDEMJIGRcWlSDifSLoKG37UCOH/DPeG52/xwois= | ||||
| github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f/go.mod h1:AQiQKKI/YIIctvDt3hI3c1S05/JXMM7v/sQcRd0paVE= | ||||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||
| github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E= | ||||
| github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= | ||||
| github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= | ||||
| github.com/Philipp15b/go-steam v1.0.1-0.20190816133340-b04c5a83c1c0 h1:TO7d4rocnNFng6ZQrPe7U6WqHtK5eHEMrgrnnM/72IQ= | ||||
| github.com/Philipp15b/go-steam v1.0.1-0.20190816133340-b04c5a83c1c0/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg= | ||||
| github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA= | ||||
| github.com/Rhymen/go-whatsapp v0.1.1-0.20200408093540-2f227c53b44f h1:uclEol7RbpElhXXmwu38PDeGcgMXNU2vh5DWwzlg7xI= | ||||
| github.com/Rhymen/go-whatsapp v0.1.1-0.20200408093540-2f227c53b44f/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk= | ||||
| github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME= | ||||
| github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU= | ||||
| github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw= | ||||
| github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM= | ||||
| github.com/StackExchange/wmi v0.0.0-20170410192909-ea383cf3ba6e/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= | ||||
| github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= | ||||
| github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= | ||||
| github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58 h1:MkpmYfld/S8kXqTYI68DfL8/hHXjHogL120Dy00TIxc= | ||||
| github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= | ||||
| github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= | ||||
| github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560 h1:ItnC9PEEMESzTbFayxrhKBbuFQOXDBI8yy7NudTcEWs= | ||||
| github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560/go.mod h1:o38AwUFFS4gzbjSoyIgrZ1h9UeDrKwcci1Pj6baifvI= | ||||
| github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c h1:4mIZQXKYBymQ9coA82nNyG/CjicMNLBZ8cPVrhNUM3g= | ||||
| github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c/go.mod h1:DNSFRLFDFIqm2+0aJzSOVfn25020vldM4SRqz6YtLgI= | ||||
| github.com/SevereCloud/vksdk/v2 v2.17.0 h1:Wll63JSuBTdE0L7+V/PMn9PyhLrWSWIjX76XpWbXTFw= | ||||
| github.com/SevereCloud/vksdk/v2 v2.17.0/go.mod h1:y3q3XAdqnQ2Wf0B+Wi7qNdqJc5ZZsz4ve+DoSQsrChk= | ||||
| github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58/go.mod h1:YNfsMyWSs+h+PaYkxGeMVmVCX75Zj/pqdjbu12ciCYE= | ||||
| github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= | ||||
| github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= | ||||
| github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= | ||||
| github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= | ||||
| github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= | ||||
| github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= | ||||
| github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= | ||||
| github.com/av-elier/go-decimal-to-rational v0.0.0-20191127152832-89e6aad02ecf h1:csfEAyvOG4/498Q4SyF48ysFqQC9ESj3o8ppRtg+Rog= | ||||
| github.com/av-elier/go-decimal-to-rational v0.0.0-20191127152832-89e6aad02ecf/go.mod h1:POPnOeaYF7U9o3PjLTb9icRfEOxjBNLRXh9BLximJGM= | ||||
| github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= | ||||
| github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= | ||||
| github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= | ||||
| github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||||
| github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= | ||||
| github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= | ||||
| github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= | ||||
| github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= | ||||
| github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= | ||||
| github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= | ||||
| github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= | ||||
| github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= | ||||
| github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= | ||||
| github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||
| github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= | ||||
| github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= | ||||
| github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= | ||||
| github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= | ||||
| github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= | ||||
| github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= | ||||
| github.com/d5/tengo/v2 v2.1.2 h1:JR5O6qJW2GW9lpv/MfEqK16a/Wpp2y8I0JZZ5fqNOL0= | ||||
| github.com/d5/tengo/v2 v2.1.2/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0DZC8= | ||||
| github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= | ||||
| github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | ||||
| github.com/d5/tengo/v2 v2.17.0 h1:BWUN9NoJzw48jZKiYDXDIF3QrIVZRm1uV1gTzeZ2lqM= | ||||
| github.com/d5/tengo/v2 v2.17.0/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0DZC8= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec h1:JEUiu7P9smN7zgX87a2zVnnbPPickIM9Gf9OIhsIgWQ= | ||||
| github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec/go.mod h1:UGa5M2Sz/Uh13AMse4+RELKCDw7kqgqlTjeGae+7vUY= | ||||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= | ||||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | ||||
| github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= | ||||
| github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= | ||||
| github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= | ||||
| github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dchote/go-openal v0.0.0-20171116030048-f4a9a141d372/go.mod h1:74z+CYu2/mx4N+mcIS/rsvfAxBPBV9uv8zRAnwyFkdI= | ||||
| github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= | ||||
| github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | ||||
| github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | ||||
| github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64= | ||||
| github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg= | ||||
| github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= | ||||
| github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | ||||
| github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= | ||||
| github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= | ||||
| github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= | ||||
| github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= | ||||
| github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= | ||||
| github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= | ||||
| github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= | ||||
| github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= | ||||
| github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= | ||||
| github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | ||||
| github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= | ||||
| github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= | ||||
| github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= | ||||
| github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= | ||||
| github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | ||||
| github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= | ||||
| github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= | ||||
| github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= | ||||
| github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= | ||||
| github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= | ||||
| github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= | ||||
| github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= | ||||
| github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= | ||||
| github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible h1:i64CCJcSqkRIkm5OSdZQjZq84/gJsk2zNwHWIRYWlKE= | ||||
| github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= | ||||
| github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= | ||||
| github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= | ||||
| github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||
| github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||
| github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= | ||||
| github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= | ||||
| github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= | ||||
| github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= | ||||
| github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | ||||
| github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= | ||||
| github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= | ||||
| github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= | ||||
| github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= | ||||
| github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= | ||||
| github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/gomarkdown/markdown v0.0.0-20200127000047-1813ea067497 h1:wJkj+x9gPYlDyM34C6r3SXPs270coWeh85wu1CsusDo= | ||||
| github.com/gomarkdown/markdown v0.0.0-20200127000047-1813ea067497/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU= | ||||
| github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= | ||||
| github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= | ||||
| github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= | ||||
| github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= | ||||
| github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= | ||||
| github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= | ||||
| github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= | ||||
| github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= | ||||
| github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | ||||
| github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= | ||||
| github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= | ||||
| github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= | ||||
| github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 h1:yEt5djSYb4iNtmV9iJGVday+i4e9u6Mrn5iP64HH5QM= | ||||
| github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= | ||||
| github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= | ||||
| github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= | ||||
| github.com/google/gops v0.3.6 h1:6akvbMlpZrEYOuoebn2kR+ZJekbZqJ28fJXTs84+8to= | ||||
| github.com/google/gops v0.3.6/go.mod h1:RZ1rH95wsAGX4vMWKmqBOIWynmWisBf4QFdgT/k/xOI= | ||||
| github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||
| github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||
| github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= | ||||
| github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||
| github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||
| github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= | ||||
| github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= | ||||
| github.com/google/gops v0.3.27 h1:BDdWfedShsBbeatZ820oA4DbVOC8yJ4NI8xAlDFWfgI= | ||||
| github.com/google/gops v0.3.27/go.mod h1:lYqabmfnq4Q6UumWNx96Hjup5BDAVc8zmfIy0SkNCSk= | ||||
| github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= | ||||
| github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= | ||||
| github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= | ||||
| github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= | ||||
| github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 h1:4EZlYQIiyecYJlUbVkFXCXHz1QPhVXcHnQKAzBTPfQo= | ||||
| github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4/go.mod h1:lEO7XoHJ/xNRBCxrn4h/CEB67h0kW1B0t4ooP2yrjUA= | ||||
| github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= | ||||
| github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= | ||||
| github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= | ||||
| github.com/gopackage/ddp v0.0.3 h1:fd0DxScoiS+ogq22ktey6DjDSDybtJPAn69geMpUtFc= | ||||
| github.com/gopackage/ddp v0.0.3/go.mod h1:3hUXYG6C/6JsoxKsQaK7st09+GP9RZBFPzyAlU/0SLg= | ||||
| github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= | ||||
| github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= | ||||
| github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= | ||||
| github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= | ||||
| github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= | ||||
| github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= | ||||
| github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= | ||||
| github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= | ||||
| github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= | ||||
| github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= | ||||
| github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= | ||||
| github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= | ||||
| github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= | ||||
| github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= | ||||
| github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= | ||||
| github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= | ||||
| github.com/harmony-development/shibshib v0.0.0-20220101224523-c98059d09cfa h1:0EefSRfsNrdEwmoGVz4+cMG8++5M2XhvJ1tTRmmrJu8= | ||||
| github.com/harmony-development/shibshib v0.0.0-20220101224523-c98059d09cfa/go.mod h1:+KEOMb29OC2kRa5BajwNM2NEjHTbQA/Z3gKYARLHREI= | ||||
| github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= | ||||
| github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= | ||||
| github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= | ||||
| github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= | ||||
| github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= | ||||
| github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= | ||||
| github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= | ||||
| github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= | ||||
| github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= | ||||
| github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= | ||||
| github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= | ||||
| github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | ||||
| github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | ||||
| github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= | ||||
| github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= | ||||
| github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= | ||||
| github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= | ||||
| github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= | ||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||
| github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= | ||||
| github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= | ||||
| github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= | ||||
| github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= | ||||
| github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= | ||||
| github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= | ||||
| github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= | ||||
| github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= | ||||
| github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= | ||||
| github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= | ||||
| github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | ||||
| github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= | ||||
| github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro= | ||||
| github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= | ||||
| github.com/keybase/go-keybase-chat-bot v0.0.0-20200226211841-4e48f3eaef3e h1:KbPTfR/PYuau1IzKoE4lnd8yby5I2pBj+VR6fSVbYU8= | ||||
| github.com/keybase/go-keybase-chat-bot v0.0.0-20200226211841-4e48f3eaef3e/go.mod h1:vNc28YFzigVJod0j5EbuTtRIe7swx8vodh2yA4jZ2s8= | ||||
| github.com/keybase/go-ps v0.0.0-20161005175911-668c8856d999 h1:2d+FLQbz4xRTi36DO1qYNUwfORax9XcQ0jhbO81Vago= | ||||
| github.com/keybase/go-ps v0.0.0-20161005175911-668c8856d999/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ= | ||||
| github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= | ||||
| github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | ||||
| github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | ||||
| github.com/kettek/apng v0.0.0-20191108220231-414630eed80f h1:dnCYnTSltLuPMfc7dMrkz2uBUcEf/OFBR8yRh3oRT98= | ||||
| github.com/kettek/apng v0.0.0-20191108220231-414630eed80f/go.mod h1:x78/VRQYKuCftMWS0uK5e+F5RJ7S4gSlESRWI0Prl6Q= | ||||
| github.com/keybase/go-keybase-chat-bot v0.0.0-20221220212439-e48d9abd2c20 h1:imPu/fy8VhpzD318SnouRyvIgJAoouEyph6+7XAZbbk= | ||||
| github.com/keybase/go-keybase-chat-bot v0.0.0-20221220212439-e48d9abd2c20/go.mod h1:Yan1Krk0q1FglHdCkNrF5hFQ4Sgq8LnuG4gI2U4xQAk= | ||||
| github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | ||||
| github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= | ||||
| github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||||
| github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= | ||||
| github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= | ||||
| github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= | ||||
| github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||
| github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | ||||
| github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||||
| github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= | ||||
| github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||
| github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= | ||||
| github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||
| github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= | ||||
| github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= | ||||
| github.com/labstack/echo/v4 v4.1.13 h1:JYgKq6NQQSaKbQcsOadAKX1kUVLCUzLGwu8sxN5tC34= | ||||
| github.com/labstack/echo/v4 v4.1.13/go.mod h1:3WZNypykZ3tnqpF2Qb4fPg27XDunFqgP3HGDmCMgv7U= | ||||
| github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= | ||||
| github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= | ||||
| github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 h1:BS9tqL0OCiOGuy/CYYk2gc33fxqaqh5/rhqMKu4tcYA= | ||||
| github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7/go.mod h1:liX5MxHPrwgHaKowoLkYGwbXfYABh1jbZ6FpElbGF1I= | ||||
| github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= | ||||
| github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= | ||||
| github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d h1:F+Sr+C0ojSlYQ37BLylQtSFmyQULe3jbAygcyXQ9mVs= | ||||
| github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d/go.mod h1:c6MxwqHD+0HvtAJjsHMIdPCiAwGiQwPRPTp69ACMg8A= | ||||
| github.com/matterbridge/discordgo v0.18.1-0.20200308151012-aa40f01cbcc3 h1:VP/DNRn2HtrVRN6+X3h4FDcQI2OOKT+88WUi21ZD1Kw= | ||||
| github.com/matterbridge/discordgo v0.18.1-0.20200308151012-aa40f01cbcc3/go.mod h1:5a1bHtG/38ofcx9cgwM5eTW/Pl4SpbQksNDnTRcGA2Y= | ||||
| github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible h1:oaOqwbg5HxHRxvAbd84ks0Okwoc1ISyUZ87EiVJFhGI= | ||||
| github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible/go.mod h1:igE6rUAn3jai2wCdsjFHfhUoekjrFthoEjFObKKwSb4= | ||||
| github.com/matterbridge/go-xmpp v0.0.0-20200329150250-5812999b292b h1:ZYI2HCj9zPzI4Si1ouSOi/ImA2xSQLUCJPQsLWr8FE0= | ||||
| github.com/matterbridge/go-xmpp v0.0.0-20200329150250-5812999b292b/go.mod h1:ECDRehsR9TYTKCAsRS8/wLeOk6UUqDydw47ln7wG41Q= | ||||
| github.com/matterbridge/gomatrix v0.0.0-20200209224845-c2104d7936a6 h1:Kl65VJv38HjYFnnwH+MP6Z8hcJT5UHuSpHVU5vW1HH0= | ||||
| github.com/matterbridge/gomatrix v0.0.0-20200209224845-c2104d7936a6/go.mod h1:+jWeaaUtXQbBRdKYWfjW6JDDYiI2XXE+3NnTjW5kg8g= | ||||
| github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18 h1:fLhwXtWGtfTgZVxHG1lcKjv+re7dRwyyuYFNu69xdho= | ||||
| github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18/go.mod h1:yAjnZ34DuDyPHMPHHjOsTk/FefW4JJjoMMCGt/8uuQA= | ||||
| github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61 h1:R/MgM/eUyRBQx2FiH6JVmXck8PaAuKfe2M1tWIzW7nE= | ||||
| github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61/go.mod h1:iXGEotOvwI1R1SjLxRc+BF5rUORTMtE0iMZBT2lxqAU= | ||||
| github.com/mattermost/mattermost-server v5.5.0+incompatible h1:0wcLGgYtd+YImtLDPf2AOfpBHxbU4suATx+6XKw1XbU= | ||||
| github.com/mattermost/mattermost-server v5.5.0+incompatible/go.mod h1:5L6MjAec+XXQwMIt791Ganu45GKsSiM+I0tLR9wUj8Y= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||
| github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= | ||||
| github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= | ||||
| github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= | ||||
| github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= | ||||
| github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= | ||||
| github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= | ||||
| github.com/lrstanley/girc v0.0.0-20240823210506-80555f2adb03 h1:faIblcMAYLrio/galHoXD0opafk7Ajkjlv4di3Gs8Mc= | ||||
| github.com/lrstanley/girc v0.0.0-20240823210506-80555f2adb03/go.mod h1:lgrnhcF8bg/Bd5HA5DOb4Z+uGqUqGnp4skr+J2GwVgI= | ||||
| github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= | ||||
| github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= | ||||
| github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= | ||||
| github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= | ||||
| github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696 h1:pmPKkN3RJM9wVMZidR99epzK0+gatQiqVtvP1FacZcQ= | ||||
| github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696/go.mod h1:c6MxwqHD+0HvtAJjsHMIdPCiAwGiQwPRPTp69ACMg8A= | ||||
| github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8 h1:UkpezVlW6/j4qB2FSsz4na2FfZUbIDMAblpjw0Cgqcg= | ||||
| github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8/go.mod h1:Vl95jJ8rOBCHUR++A1n6nM5sMk1oHcwInsaxRTR9NM4= | ||||
| github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27 h1:9XSppnbvvReVom+wphkeF4lbhuT6vCYIdyzpwFtW89c= | ||||
| github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27/go.mod h1:/x38AoZf70fK9yZ5gs3BNCaF7/J4QEo4ZpwtLjX95eQ= | ||||
| github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75 h1:GslZKF7lW7oSisycGLpxPO+TnKJuA4VZuTWIfYZrClc= | ||||
| github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75/go.mod h1:yAjnZ34DuDyPHMPHHjOsTk/FefW4JJjoMMCGt/8uuQA= | ||||
| github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba h1:XleOY4IjAEIcxAh+IFwT5JT5Ze3RHiYz6m+4ZfZ0rc0= | ||||
| github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba/go.mod h1:iXGEotOvwI1R1SjLxRc+BF5rUORTMtE0iMZBT2lxqAU= | ||||
| github.com/matterbridge/matterclient v0.0.0-20240817214420-3d4c3aef3dc1 h1:LHzlbT3VIcRFhSPwKpZfT2TU2rUzuA8liF57gSEBuaI= | ||||
| github.com/matterbridge/matterclient v0.0.0-20240817214420-3d4c3aef3dc1/go.mod h1:quMZkUhnTp/YfbJU6sCgD6qVSPgH1Uxig9/AjgJQHF8= | ||||
| github.com/matterbridge/telegram-bot-api/v6 v6.5.0 h1:wCnHWvt4WGhfognQsuu2OnHyqENBdJRf2mReYTCXggQ= | ||||
| github.com/matterbridge/telegram-bot-api/v6 v6.5.0/go.mod h1:/hSLrs8h/xNsQglQXjwXJ92iZU8XfTGkYUQ7KVDWEVo= | ||||
| github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= | ||||
| github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= | ||||
| github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI= | ||||
| github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI= | ||||
| github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy52be4= | ||||
| github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc= | ||||
| github.com/mattermost/mattermost/server/public v0.1.6 h1:k3upRnYfDY9CtVifQ+hQS6ZP4dGsU6I3oG1DCcBYy0c= | ||||
| github.com/mattermost/mattermost/server/public v0.1.6/go.mod h1:Dm5uf3z8ckDOKYD1cbnb1Uqm/G9WYIaouSP/HnH+Rbs= | ||||
| github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= | ||||
| github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= | ||||
| github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= | ||||
| github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= | ||||
| github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= | ||||
| github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= | ||||
| github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | ||||
| github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= | ||||
| github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | ||||
| github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | ||||
| github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= | ||||
| github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= | ||||
| github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= | ||||
| github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= | ||||
| github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= | ||||
| github.com/mattn/godown v0.0.0-20180312012330-2e9e17e0ea51 h1:MpI7hy3MiCnrggmZI/s8LaPbLVOOWpzDbjA4F+XaXaM= | ||||
| github.com/mattn/godown v0.0.0-20180312012330-2e9e17e0ea51/go.mod h1:s3KUdOIXJ+jaGM++XHiXA6gikdleaWVATCcQGD4h734= | ||||
| github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||||
| github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||||
| github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||||
| github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||||
| github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= | ||||
| github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= | ||||
| github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||||
| github.com/mattn/godown v0.0.1 h1:39uk50ufLVQFs0eapIJVX5fCS74a1Fs2g5f1MVqIHdE= | ||||
| github.com/mattn/godown v0.0.1/go.mod h1:/ivCKurgV/bx6yqtP/Jtc2Xmrv3beCYBvlfAUl4X5g4= | ||||
| github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= | ||||
| github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= | ||||
| github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c= | ||||
| github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= | ||||
| github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= | ||||
| github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= | ||||
| github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= | ||||
| github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 h1:oKIteTqeSpenyTrOVj5zkiyCaflLa8B+CD0324otT+o= | ||||
| github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= | ||||
| github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff h1:HLGD5/9UxxfEuO9DtP8gnTmNtMxbPyhYltfxsITel8g= | ||||
| github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E= | ||||
| github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= | ||||
| github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 h1:mp6tU1r0xLostUGLkTspf/9/AiHuVD7ptyXhySkDEsE= | ||||
| github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9/go.mod h1:A5SRAcpTemjGgIuBq6Kic2yHcoeUFWUinOAlMP/i9xo= | ||||
| github.com/nicksnyder/go-i18n v1.4.0 h1:AgLl+Yq7kg5OYlzCgu9cKTZOyI4tD/NgukKqLqC8E+I= | ||||
| github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= | ||||
| github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA= | ||||
| github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= | ||||
| github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= | ||||
| github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= | ||||
| github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= | ||||
| github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= | ||||
| github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= | ||||
| github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= | ||||
| github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= | ||||
| github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= | ||||
| github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= | ||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||
| github.com/monaco-io/request v1.0.5 h1:QAJb5m1pCPZUGv3zzTZn7GlQI3q+uJWi7fH9QxDGbm4= | ||||
| github.com/monaco-io/request v1.0.5/go.mod h1:EmggwHktBsbJmCgwZXqy7o0H1NNsAstQBWZrFVd3xtQ= | ||||
| github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= | ||||
| github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= | ||||
| github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= | ||||
| github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= | ||||
| github.com/nelsonken/gomf v0.0.0-20190423072027-c65cc0469e94 h1:GRBho7BMw6le5OVegxDeFX+Ul1WPykzQs9CVrifrjik= | ||||
| github.com/nelsonken/gomf v0.0.0-20190423072027-c65cc0469e94/go.mod h1:UT7a3UHzG7jEEeoW/AI2/e0RuLbkntdsiY94qt6Jv7Q= | ||||
| github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= | ||||
| github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= | ||||
| github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= | ||||
| github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= | ||||
| github.com/olahol/melody v1.2.1 h1:xdwRkzHxf+B0w4TKbGpUSSkV516ZucQZJIWLztOWICQ= | ||||
| github.com/olahol/melody v1.2.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= | ||||
| github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U= | ||||
| github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= | ||||
| github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= | ||||
| github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= | ||||
| github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | ||||
| github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= | ||||
| github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= | ||||
| github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= | ||||
| github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c h1:P6XGcuPTigoHf4TSu+3D/7QOQ1MbL6alNwrGhcW7sKw= | ||||
| github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c/go.mod h1:YnNlZP7l4MhyGQ4CBRwv6ohZTPrUJJZtEv4ZgADkbs4= | ||||
| github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 h1:/CPgDYrfeK2LMK6xcUhvI17yO9SlpAdDIJGkhDEgO8A= | ||||
| github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= | ||||
| github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= | ||||
| github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= | ||||
| github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= | ||||
| github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= | ||||
| github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= | ||||
| github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= | ||||
| github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= | ||||
| github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= | ||||
| github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= | ||||
| github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 h1:jYi87L8j62qkXzaYHAQAhEapgukhenIMZRBKTNRLHJ4= | ||||
| github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= | ||||
| github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | ||||
| github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= | ||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= | ||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | ||||
| github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= | ||||
| github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||
| github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= | ||||
| github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= | ||||
| github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= | ||||
| github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= | ||||
| github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= | ||||
| github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||
| github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= | ||||
| github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/rickb777/date v1.12.4 h1:+6IzcCCS/1t17DrmnEvrznyq7nM8vPwir6/UhlyohKw= | ||||
| github.com/rickb777/date v1.12.4/go.mod h1:xP0eo/I5qmUt97yRGClHZfyLZ3ikMw6v6SU5MOGZTE0= | ||||
| github.com/rickb777/plural v1.2.0 h1:5tvEc7UBCZ7l8h/2UeybSkt/uu1DQsZFOFdNevmUhlE= | ||||
| github.com/rickb777/plural v1.2.0/go.mod h1:UdpyWFCGbo3mvK3f/PfZOAOrkjzJlYN/sD46XNWJ+Es= | ||||
| github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= | ||||
| github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= | ||||
| github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= | ||||
| github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= | ||||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | ||||
| github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | ||||
| github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= | ||||
| github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= | ||||
| github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= | ||||
| github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= | ||||
| github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||
| github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= | ||||
| github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= | ||||
| github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= | ||||
| github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= | ||||
| github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= | ||||
| github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 h1:Lx3BlDGFElJt4u/zKc9A3BuGYbQAGlEFyPuUA3jeMD0= | ||||
| github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI= | ||||
| github.com/shazow/ssh-chat v1.8.3-0.20200308224626-80ddf1f43a98 h1:sN07ff+PSRsUNhpSod4uGKAQ+Nc0FXsBPG9FmYMNg4w= | ||||
| github.com/shazow/ssh-chat v1.8.3-0.20200308224626-80ddf1f43a98/go.mod h1:xkTgfD+WP+KR4HuG76oal25BBEeu5kJyi2EOsgiu/4Q= | ||||
| github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= | ||||
| github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= | ||||
| github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= | ||||
| github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= | ||||
| github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= | ||||
| github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= | ||||
| github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= | ||||
| github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= | ||||
| github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= | ||||
| github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= | ||||
| github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= | ||||
| github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= | ||||
| github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= | ||||
| github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= | ||||
| github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 h1:zwQ1HBo5FYwn1ksMd19qBCKO8JAWE9wmHivEpkw/DvE= | ||||
| github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI= | ||||
| github.com/shazow/ssh-chat v1.10.1 h1:ePS+ngEYqm+yUuXegDPutysqLV2WoI22XDOeRgI6CE0= | ||||
| github.com/shazow/ssh-chat v1.10.1/go.mod h1:0+7szsKylcre0vljkVnbuI6q7Odtc+QCDHxa+fFNV54= | ||||
| github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= | ||||
| github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= | ||||
| github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= | ||||
| github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= | ||||
| github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= | ||||
| github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= | ||||
| github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= | ||||
| github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= | ||||
| github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= | ||||
| github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= | ||||
| github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= | ||||
| github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= | ||||
| github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= | ||||
| github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= | ||||
| github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= | ||||
| github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= | ||||
| github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= | ||||
| github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= | ||||
| github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= | ||||
| github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= | ||||
| github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= | ||||
| github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= | ||||
| github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= | ||||
| github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= | ||||
| github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 h1:A7o8tOERTtpD/poS+2VoassCjXpjHn916luXbf5QKD0= | ||||
| github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882/go.mod h1:5IwJoz9Pw7JsrCN4/skkxUtSWT7myuUPLhCgv6Q5vvQ= | ||||
| github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE= | ||||
| github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= | ||||
| github.com/slack-go/slack v0.6.3 h1:qU037g8gQ71EuH6S9zYKnvYrEUj0fLFH4HFekFqBoRU= | ||||
| github.com/slack-go/slack v0.6.3/go.mod h1:HE4RwNe7YpOg/F0vqo5PwXH3Hki31TplTvKRW9dGGaw= | ||||
| github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= | ||||
| github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | ||||
| github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= | ||||
| github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= | ||||
| github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= | ||||
| github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= | ||||
| github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= | ||||
| github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= | ||||
| github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= | ||||
| github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= | ||||
| github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= | ||||
| github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= | ||||
| github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= | ||||
| github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= | ||||
| github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk= | ||||
| github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= | ||||
| github.com/slack-go/slack v0.14.0 h1:6c0UTfbRnvRssZUsZ2qe0Iu07VAMPjRqOa6oX8ewF4k= | ||||
| github.com/slack-go/slack v0.14.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= | ||||
| github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= | ||||
| github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= | ||||
| github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= | ||||
| github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= | ||||
| github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= | ||||
| github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= | ||||
| github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= | ||||
| github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= | ||||
| github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= | ||||
| github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= | ||||
| github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= | ||||
| github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= | ||||
| github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||
| github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= | ||||
| github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||
| github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= | ||||
| github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||
| github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
| github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= | ||||
| github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= | ||||
| github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= | ||||
| github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= | ||||
| github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= | ||||
| github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= | ||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||
| github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||
| github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||||
| github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= | ||||
| github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= | ||||
| github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= | ||||
| github.com/tinylib/msgp v1.2.0 h1:0uKB/662twsVBpYUPbokj4sTSKhWFKB7LopO2kWK8lY= | ||||
| github.com/tinylib/msgp v1.2.0/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro= | ||||
| github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= | ||||
| github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= | ||||
| github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= | ||||
| github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= | ||||
| github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= | ||||
| github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= | ||||
| github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= | ||||
| github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | ||||
| github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | ||||
| github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | ||||
| github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= | ||||
| github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | ||||
| github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= | ||||
| github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= | ||||
| github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= | ||||
| github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= | ||||
| github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= | ||||
| github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= | ||||
| github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= | ||||
| github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= | ||||
| github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= | ||||
| github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= | ||||
| github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME= | ||||
| github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= | ||||
| github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= | ||||
| github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= | ||||
| github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw= | ||||
| github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= | ||||
| github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= | ||||
| github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= | ||||
| github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= | ||||
| github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= | ||||
| github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= | ||||
| github.com/yaegashi/msgraph.go v0.1.2 h1:83uVRQaj8YBsVqOUGj0WRwzxdgGF69jRpg5IQYaTvoY= | ||||
| github.com/yaegashi/msgraph.go v0.1.2/go.mod h1:Lp39e9oo596G5FcmMKI0cXR3mg/QikSdabgZdbMqbAM= | ||||
| github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447 h1:CHgPZh8bFkZmislPrr/0gd7MciDAX+JJB70A2/5Lvmo= | ||||
| github.com/zfjagann/golang-ring v0.0.0-20190106091943-a88bb6aef447/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU= | ||||
| go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= | ||||
| go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= | ||||
| go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= | ||||
| go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= | ||||
| go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= | ||||
| go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= | ||||
| go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= | ||||
| golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= | ||||
| golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||
| github.com/yaegashi/msgraph.go v0.1.4 h1:leDXSczAbwBpYFSmmZrdByTiPoUw8dbTfNMetAjJvbw= | ||||
| github.com/yaegashi/msgraph.go v0.1.4/go.mod h1:vgeYhHa5skJt/3lTyjGXThTZhwbhRnGo6uUxzoJIGME= | ||||
| github.com/yaegashi/wtz.go v0.0.2/go.mod h1:nOLA5QXsmdkRxBkP5tljhua13ADHCKirLBrzPf4PEJc= | ||||
| github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||
| github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||
| github.com/zfjagann/golang-ring v0.0.0-20220330170733-19bcea1b6289 h1:dEdcEes8Aki8XrgZFyrZvtazFlW4U7eNvX9NuyFJAtQ= | ||||
| github.com/zfjagann/golang-ring v0.0.0-20220330170733-19bcea1b6289/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU= | ||||
| go.mau.fi/libsignal v0.1.1 h1:m/0PGBh4QKP/I1MQ44ti4C0fMbLMuHb95cmDw01FIpI= | ||||
| go.mau.fi/libsignal v0.1.1/go.mod h1:QLs89F/OA3ThdSL2Wz2p+o+fi8uuQUz0e1BRa6ExdBw= | ||||
| go.mau.fi/util v0.6.0 h1:W6SyB3Bm/GjenQ5iq8Z8WWdN85Gy2xS6L0wmnR7SVjg= | ||||
| go.mau.fi/util v0.6.0/go.mod h1:ljYdq3sPfpICc3zMU+/mHV/sa4z0nKxc67hSBwnrk8U= | ||||
| go.mau.fi/whatsmeow v0.0.0-20240821142752-3d63c6fcc1a7 h1:Aa4uov0rM0SQQ7Fc/TZZpmQEGksie2SVTv/UuCJwViI= | ||||
| go.mau.fi/whatsmeow v0.0.0-20240821142752-3d63c6fcc1a7/go.mod h1:BhHKalSq0qNtSCuGIUIvoJyU5KbT4a7k8DQ5yw1Ssk4= | ||||
| go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= | ||||
| go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= | ||||
| go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= | ||||
| go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= | ||||
| golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= | ||||
| golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||
| golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc= | ||||
| golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/image v0.0.0-20191214001246-9130b4cfad52 h1:2fktqPPvDiVEEVT/vSTeoUPXfmRxRaGy6GU8jypvEn0= | ||||
| golang.org/x/image v0.0.0-20191214001246-9130b4cfad52/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= | ||||
| golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= | ||||
| golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= | ||||
| golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= | ||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||
| golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= | ||||
| golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= | ||||
| golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= | ||||
| golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= | ||||
| golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | ||||
| golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | ||||
| golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= | ||||
| golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||
| golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= | ||||
| golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||||
| golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= | ||||
| golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= | ||||
| golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | ||||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= | ||||
| golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= | ||||
| golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= | ||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||
| golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= | ||||
| golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||
| golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||
| golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||
| golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||
| golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= | ||||
| golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | ||||
| golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= | ||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sys v0.0.0-20171017063910-8dbc5d05d6ed/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= | ||||
| golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||
| golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM= | ||||
| golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= | ||||
| golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= | ||||
| golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= | ||||
| golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||
| golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||
| golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= | ||||
| golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= | ||||
| golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= | ||||
| golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | ||||
| golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= | ||||
| golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||
| golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||
| golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.0.0-20200529172331-a64b76657301/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= | ||||
| golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= | ||||
| golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= | ||||
| golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| gomod.garykim.dev/nc-talk v0.3.0 h1:MZxLc/gX2/+bdOw4xt6pi+qQFUQld1woGfw1hEJ0fbM= | ||||
| gomod.garykim.dev/nc-talk v0.3.0/go.mod h1:q/Adot/H7iqi+H4lANopV7/xcMf+sX3AZXUXqiITwok= | ||||
| google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= | ||||
| google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= | ||||
| google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= | ||||
| google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | ||||
| google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= | ||||
| google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | ||||
| google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | ||||
| google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | ||||
| google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | ||||
| google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | ||||
| google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | ||||
| google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= | ||||
| google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= | ||||
| google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= | ||||
| google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= | ||||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo= | ||||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= | ||||
| google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= | ||||
| google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= | ||||
| google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= | ||||
| google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= | ||||
| google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= | ||||
| gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= | ||||
| google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= | ||||
| google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= | ||||
| google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= | ||||
| google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= | ||||
| google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= | ||||
| google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= | ||||
| google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= | ||||
| google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= | ||||
| google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= | ||||
| google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||
| google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||
| google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||
| google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= | ||||
| google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||||
| google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= | ||||
| google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= | ||||
| google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= | ||||
| google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= | ||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= | ||||
| gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | ||||
| gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | ||||
| gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= | ||||
| gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||
| gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= | ||||
| gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= | ||||
| gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= | ||||
| gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= | ||||
| gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= | ||||
| gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||
| gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= | ||||
| gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
| gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= | ||||
| gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= | ||||
| gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | ||||
| gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= | ||||
| honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| rsc.io/goversion v1.0.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= | ||||
| honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa/go.mod h1:AOef7vHz0+v4sWwJnr0jSyHiX/1NgsMoaxl+rEPz/I0= | ||||
| layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14 h1:wY8eeq7DpM5iAugNbFrvuhdtmN8XM1iU+Ki5YZWjukg= | ||||
| layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14/go.mod h1:tWPVA9ZAfImNwabjcd9uDE+Mtz0Hfs7a7G3vxrnrwyc= | ||||
| modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= | ||||
| modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= | ||||
| modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= | ||||
| modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= | ||||
| modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= | ||||
| modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= | ||||
| modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= | ||||
| modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= | ||||
| modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= | ||||
| modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= | ||||
| modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= | ||||
| modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= | ||||
| modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= | ||||
| modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= | ||||
| modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= | ||||
| modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= | ||||
| modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= | ||||
| modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= | ||||
| modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= | ||||
| modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= | ||||
| modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= | ||||
| modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= | ||||
| modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= | ||||
| modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= | ||||
| modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= | ||||
| modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= | ||||
| rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= | ||||
| rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= | ||||
| sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= | ||||
| sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= | ||||
|   | ||||
| @@ -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 | 
| @@ -4,20 +4,19 @@ import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
|  | ||||
| 	"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.17.2" | ||||
| 	githash string | ||||
|  | ||||
| 	flagConfig  = flag.String("conf", "matterbridge.toml", "config file") | ||||
| 	flagDebug   = flag.Bool("debug", false, "enable debug") | ||||
| 	flagVersion = flag.Bool("version", false, "show version") | ||||
| @@ -27,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 | ||||
| 	} | ||||
|  | ||||
| @@ -42,14 +41,23 @@ 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.") | ||||
| 	} | ||||
|  | ||||
| 	cfg := config.NewConfig(rootLogger, *flagConfig) | ||||
| 	cfg.BridgeValues().General.Debug = *flagDebug | ||||
|  | ||||
| 	// if logging to a file, ensure it is closed when the program terminates | ||||
| 	// nolint:errcheck | ||||
| 	defer func() { | ||||
| 		if f, ok := rootLogger.Out.(*os.File); ok { | ||||
| 			f.Sync() | ||||
| 			f.Close() | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	r, err := gateway.NewRouter(rootLogger, cfg, bridgemap.FullMap) | ||||
| 	if err != nil { | ||||
| 		logger.Fatalf("Starting gateway failed: %s", err) | ||||
| @@ -57,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 {} | ||||
| } | ||||
|  | ||||
| @@ -67,17 +75,31 @@ func setupLogger() *logrus.Logger { | ||||
| 		Formatter: &prefixed.TextFormatter{ | ||||
| 			PrefixPadding: 13, | ||||
| 			DisableColors: true, | ||||
| 			FullTimestamp: true, | ||||
| 		}, | ||||
| 		Level: logrus.InfoLevel, | ||||
| 	} | ||||
| 	if *flagDebug || os.Getenv("DEBUG") == "1" { | ||||
| 		logger.SetReportCaller(true) | ||||
| 		logger.Formatter = &prefixed.TextFormatter{ | ||||
| 			PrefixPadding:   13, | ||||
| 			DisableColors:   true, | ||||
| 			FullTimestamp:   false, | ||||
| 			ForceFormatting: true, | ||||
| 			PrefixPadding: 13, | ||||
| 			DisableColors: true, | ||||
| 			FullTimestamp: false, | ||||
|  | ||||
| 			CallerFormatter: func(function, file string) string { | ||||
| 				return fmt.Sprintf(" [%s:%s]", function, file) | ||||
| 			}, | ||||
| 			CallerPrettyfier: func(f *runtime.Frame) (string, string) { | ||||
| 				sp := strings.SplitAfter(f.File, "/matterbridge/") | ||||
| 				filename := f.File | ||||
| 				if len(sp) > 1 { | ||||
| 					filename = sp[1] | ||||
| 				} | ||||
| 				s := strings.Split(f.Function, ".") | ||||
| 				funcName := s[len(s)-1] | ||||
| 				return funcName, fmt.Sprintf("%s:%d", filename, f.Line) | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		logger.Level = logrus.DebugLevel | ||||
| 		logger.WithFields(logrus.Fields{"prefix": "main"}).Info("Enabling debug logging.") | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										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,21 @@ 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 | ||||
| #String | ||||
| #OPTIONAL (default 1m) | ||||
| PingDelay="1m" | ||||
|  | ||||
| #StripMarkdown strips markdown from messages | ||||
| #OPTIONAL (default false) | ||||
| StripMarkdown=false | ||||
|  | ||||
| #Nicks you want to ignore. | ||||
| #Regular expressions supported | ||||
| @@ -128,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"] ] | ||||
|  | ||||
| @@ -153,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 | ||||
|  | ||||
| @@ -163,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 | ||||
|  | ||||
| @@ -177,6 +212,25 @@ StripNick=false | ||||
| #OPTIONAL (default false) | ||||
| ShowTopicChange=false | ||||
|  | ||||
| #Delay in milliseconds between channel joins | ||||
| #Only useful when you have a LOT of channels to join | ||||
| #See https://github.com/42wim/matterbridge/issues/1084 | ||||
| #OPTIONAL (default 0) | ||||
| JoinDelay=0 | ||||
|  | ||||
| #Use the optional RELAYMSG extension for username spoofing on IRC. | ||||
| #This requires an IRCd that supports the draft/relaymsg specification: currently this includes | ||||
| #Oragono 2.4.0+ and InspIRCd 3 with the m_relaymsg contrib module. | ||||
| #See https://github.com/42wim/matterbridge/issues/667#issuecomment-634214165 for more details. | ||||
| #Spoofed nicks will use the configured RemoteNickFormat, replacing reserved IRC characters | ||||
| #(!+%@&#$:'"?*,.) with a hyphen (-). | ||||
| #On most configurations, the RemoteNickFormat must include a separator character such as "/". | ||||
| #You should make sure that the settings here match your IRCd. | ||||
| #This option overrides ColorNicks. | ||||
| #OPTIONAL (default false) | ||||
| UseRelayMsg=false | ||||
| #RemoteNickFormat="{NICK}/{PROTOCOL}" | ||||
|  | ||||
| ################################################################### | ||||
| #XMPP section | ||||
| ################################################################### | ||||
| @@ -190,12 +244,16 @@ ShowTopicChange=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 | ||||
| @@ -211,6 +269,10 @@ Nick="xmppbot" | ||||
| #OPTIONAL (default false) | ||||
| SkipTLSVerify=true | ||||
|  | ||||
| #Enable to use plaintext connection to your XMPP server. | ||||
| #OPTIONAL (default false) | ||||
| NoTLS=true | ||||
|  | ||||
| ## RELOADABLE SETTINGS | ||||
| ## Settings below can be reloaded by editing the file | ||||
|  | ||||
| @@ -239,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"] ] | ||||
|  | ||||
| @@ -263,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 | ||||
|  | ||||
| @@ -277,6 +339,11 @@ StripNick=false | ||||
| #OPTIONAL (default false) | ||||
| ShowTopicChange=false | ||||
|  | ||||
| #Enable sending messages using a webhook instead of regular MUC messages. | ||||
| #Only works with a prosody server using mod_slack_webhook. Does not support editing. | ||||
| #OPTIONAL (default "") | ||||
| WebhookURL="https://yourdomain/prosody/msg/someid" | ||||
|  | ||||
| ################################################################### | ||||
| #mattermost section | ||||
| ################################################################### | ||||
| @@ -342,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) | ||||
| @@ -397,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"] ] | ||||
|  | ||||
| @@ -421,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 | ||||
|  | ||||
| @@ -442,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 | ||||
| @@ -592,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"] ] | ||||
|  | ||||
| @@ -616,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 | ||||
|  | ||||
| @@ -731,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"] ] | ||||
|  | ||||
| @@ -755,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 | ||||
|  | ||||
| @@ -788,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 | ||||
| ################################################################### | ||||
| @@ -810,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 | ||||
|  | ||||
| @@ -826,10 +836,11 @@ UseUserName=false | ||||
| # UseDiscriminator appends the `#xxxx` discriminator when used with UseUserName | ||||
| UseDiscriminator=false | ||||
|  | ||||
| # WebhookURL sends messages in the style of puppets. | ||||
| # This only works if you have one discord channel, if you have multiple discord channels you'll have to specify it in the gateway config | ||||
| # Example: "https://discordapp.com/api/webhooks/1234/abcd_xyzw" | ||||
| WebhookURL="" | ||||
| # AutoWebhooks automatically configures message sending in the style of puppets. | ||||
| # This is an easier alternative to manually configuring "WebhookURL" for each gateway, | ||||
| # as turning this on will automatically load or create webhooks for each channel. | ||||
| # This feature requires the "Manage Webhooks" permission (either globally or as per-channel). | ||||
| AutoWebhooks=false | ||||
|  | ||||
| # EditDisable disables sending of edits to other bridges | ||||
| EditDisable=false | ||||
| @@ -914,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 | ||||
| ################################################################### | ||||
| @@ -949,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. | ||||
| @@ -972,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 | ||||
| @@ -1005,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"] ] | ||||
|  | ||||
| @@ -1033,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 | ||||
|  | ||||
| @@ -1047,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 | ||||
| ################################################################### | ||||
| @@ -1140,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"] ] | ||||
|  | ||||
| @@ -1164,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 | ||||
|  | ||||
| @@ -1191,27 +1232,41 @@ 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) | ||||
| #OPTIONAL (default false) | ||||
| NoHomeServerSuffix=false | ||||
|  | ||||
| #Whether to disable sending of HTML content to matrix | ||||
| #See https://github.com/42wim/matterbridge/issues/1022 | ||||
| #OPTIONAL (default false) | ||||
| HTMLDisable=false | ||||
|  | ||||
| ## RELOADABLE SETTINGS | ||||
| ## Settings below can be reloaded by editing the file | ||||
|  | ||||
| #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 | ||||
| # UseUserName shows the username instead of the server nickname | ||||
| UseUserName=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 | ||||
| @@ -1238,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"] ] | ||||
|  | ||||
| @@ -1262,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) | ||||
| @@ -1330,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"] ] | ||||
|  | ||||
| @@ -1354,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 | ||||
|  | ||||
| @@ -1368,12 +1428,100 @@ StripNick=false | ||||
| #OPTIONAL (default false) | ||||
| ShowTopicChange=false | ||||
|  | ||||
| ################################################################### | ||||
| # NCTalk (Nextcloud Talk) | ||||
| ################################################################### | ||||
|  | ||||
| [nctalk.bridge] | ||||
|  | ||||
| # Url of your Nextcloud server | ||||
| Server = "https://cloud.youdomain.me" | ||||
|  | ||||
| # Enable to not verify the certificate on your Nextcloud server. | ||||
| # e.g. when using selfsigned certificates | ||||
| # OPTIONAL (default false) | ||||
| SkipTLSVerify=true | ||||
|  | ||||
| # Username of the bot | ||||
| Login = "talkuser" | ||||
|  | ||||
| # Password of the bot | ||||
| Password = "talkuserpass" | ||||
|  | ||||
| # Suffix for Guest Users | ||||
| GuestSuffix = " (Guest)" | ||||
|  | ||||
| # Separate display name (Note: needs to be configured from Nextcloud Talk to work) | ||||
| SeparateDisplayName=false | ||||
|  | ||||
| ################################################################### | ||||
| # Mumble | ||||
| ################################################################### | ||||
|  | ||||
| [mumble.bridge] | ||||
|  | ||||
| # Host and port of your Mumble server | ||||
| Server = "mumble.yourdomain.me:64738" | ||||
|  | ||||
| # Nickname to log in as | ||||
| Nick = "matterbridge" | ||||
|  | ||||
| # Some servers require a password | ||||
| # OPTIONAL (default empty) | ||||
| Password = "serverpasswordhere" | ||||
|  | ||||
| # User comment to set on the Mumble user, visible to other users. | ||||
| # OPTIONAL (default empty) | ||||
| UserComment="I am bridging text messages between this channel and #general on irc.yourdomain.me" | ||||
|  | ||||
| # Self-signed TLS client certificate + private key used to connect to | ||||
| # Mumble.  This is required if you want to register the matterbridge | ||||
| # user on your Mumble server, so its nick becomes reserved. | ||||
| # You can generate a keypair using e.g. | ||||
| # | ||||
| #     openssl req -x509 -newkey rsa:2048 -nodes -days 10000 \ | ||||
| #             -keyout mumble.key -out mumble.crt | ||||
| # | ||||
| # To actually register the matterbridege user, connect to Mumble as an | ||||
| # admin, right click on the user and click "Register". | ||||
| # | ||||
| # OPTIONAL (default empty) | ||||
| TLSClientCertificate="mumble.crt" | ||||
| TLSClientKey="mumble.key" | ||||
|  | ||||
| # TLS CA certificate used to validate the Mumble server. | ||||
| # OPTIONAL (defaults to Go system CA) | ||||
| TLSCACertificate=mumble-ca.crt | ||||
|  | ||||
| # Enable to not verify the certificate on your Mumble server. | ||||
| # e.g. when using selfsigned certificates | ||||
| # OPTIONAL (default false) | ||||
| SkipTLSVerify=false | ||||
|  | ||||
| #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" | ||||
|  | ||||
| ################################################################### | ||||
| # WhatsApp | ||||
| # | ||||
| ################################################################### | ||||
|  | ||||
| [whatsapp.bridge] | ||||
| @@ -1400,9 +1548,7 @@ Label="Organization" | ||||
|  | ||||
|  | ||||
| ################################################################### | ||||
| # | ||||
| # zulip | ||||
| # | ||||
| ################################################################### | ||||
|  | ||||
| [zulip] | ||||
| @@ -1453,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"] ] | ||||
|  | ||||
| @@ -1477,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 | ||||
|  | ||||
| @@ -1491,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 | ||||
| ################################################################### | ||||
| @@ -1510,6 +1668,8 @@ Buffer=1000 | ||||
|  | ||||
| #Bearer token used for authentication | ||||
| #curl -H "Authorization: Bearer token" http://localhost:4242/api/messages | ||||
| # https://github.com/vi/websocat | ||||
| # websocat -H="Authorization: Bearer token" ws://127.0.0.1:4242/api/websocket | ||||
| #OPTIONAL (no authorization if token is empty) | ||||
| Token="mytoken" | ||||
|  | ||||
| @@ -1533,7 +1693,9 @@ RemoteNickFormat="{NICK}" | ||||
| ## Settings below can be reloaded by editing the file | ||||
|  | ||||
| #RemoteNickFormat defines how remote users appear on this bridge | ||||
| #The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. | ||||
| #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 | ||||
| #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge | ||||
| @@ -1559,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) | ||||
| @@ -1589,6 +1751,14 @@ MediaDownloadBlacklist=[".html$",".htm$"] | ||||
| #OPTIONAL (default false) | ||||
| IgnoreFailureOnStart=false | ||||
|  | ||||
| #LogFile defines the location of a file to write logs into, rather | ||||
| #than stdout. | ||||
| #Logging will still happen on stdout if the file cannot be open for | ||||
| #writing, or if the value is empty. Note that the log won't roll, so | ||||
| #you might want to use logrotate(8) with this feature. | ||||
| #OPTIONAL (default empty) | ||||
| LogFile="/var/log/matterbridge.log" | ||||
|  | ||||
| ################################################################### | ||||
| #Tengo configuration | ||||
| ################################################################### | ||||
| @@ -1600,7 +1770,7 @@ IgnoreFailureOnStart=false | ||||
| #This script will receive every incoming message and can be used to modify the Username and the Text of that message. | ||||
| #The script will have the following global variables: | ||||
| #to modify: msgUsername and msgText | ||||
| #to read: msgChannel and msgAccount | ||||
| #to read: msgUserID, msgChannel, msgAccount | ||||
| # | ||||
| #The script is reloaded on every message, so you can modify the script on the fly. | ||||
| # | ||||
| @@ -1624,9 +1794,12 @@ InMessage="example.tengo" | ||||
| #read-only: | ||||
| #inAccount, inProtocol, inChannel, inGateway, inEvent | ||||
| #outAccount, outProtocol, outChannel, outGateway, outEvent | ||||
| #msgUserID | ||||
| # | ||||
| #read-write: | ||||
| #msgText, msgUsername | ||||
| #msgText, msgUsername, msgDrop | ||||
| # | ||||
| #msgDrop is a bool which is default false, when set true this message will be dropped | ||||
| # | ||||
| #The script is reloaded on every message, so you can modify the script on the fly. | ||||
| # | ||||
| @@ -1639,7 +1812,7 @@ OutMessage="example.tengo" | ||||
| #RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script. | ||||
| #The script will have the following global variables: | ||||
| #to modify: result | ||||
| #to read: channel, bridge, gateway, protocol, nick | ||||
| #to read: channel, bridge, gateway, protocol, nick, msgUserID | ||||
| # | ||||
| #The result will be set in {TENGO} in the RemoteNickFormat key of every bridge where {TENGO} is specified | ||||
| # | ||||
| @@ -1677,9 +1850,9 @@ 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  | ||||
|     # 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 | ||||
|     # | ||||
|     # Platform   |   Identifier name  |            Example            | Description | ||||
| @@ -1694,27 +1867,35 @@ 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 | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|     #   msteams  |      threadId      |    19:82abcxx@thread.skype    | You'll find the threadId in the URL | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|     #   mumble   |    channel id      |              42               | The channel ID, as shown in the channel's "Edit" window | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|     # 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 | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|     #  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 | ||||
|     # ------------------------------------------------------------------------------------------------------------------------------------- | ||||
|  | ||||
|     # | ||||
| @@ -1729,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 | ||||
| @@ -1748,18 +1929,25 @@ enable=true | ||||
|         #OPTIONAL - your irc / xmpp channel key | ||||
|         key="yourkey" | ||||
|  | ||||
|     # Discord specific gateway options | ||||
|     [[gateway.inout]] | ||||
|     account="discord.game" | ||||
|     channel="mygreatgame" | ||||
|  | ||||
|         #OPTIONAL - webhookurl only works for discord (it needs a different URL for each cahnnel) | ||||
|         [gateway.inout.options] | ||||
|         webhookurl="https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y" | ||||
|         # WebhookURL sends messages in the style of "puppets". You must configure a webhook URL for each channel you want to bridge. | ||||
|         # If you have more than one channel and don't wnat to configure each channel manually, see the "AutoWebhooks" option in the gateway config. | ||||
|         # Example: "https://discord.com/api/webhooks/1234/abcd_xyzw" | ||||
|         WebhookURL="" | ||||
|  | ||||
|     [[gateway.inout]] | ||||
|     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/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, "") | ||||
| 	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/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, "") | ||||
| 		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,285 +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/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 | ||||
| } | ||||
|  | ||||
| // 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) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| 				} | ||||
| 			} | ||||
| 			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) | ||||
| 	} | ||||
| } | ||||