Compare commits
	
		
			393 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 52d4705a91 | ||
|   | 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 | ||
|   | d97b077e85 | ||
|   | 8950575bfb | ||
|   | 11fc4c286f | ||
|   | 8d08e348a9 | ||
|   | a18807f19e | ||
|   | 29f658fd3c | ||
|   | a30bb8fed0 | ||
|   | 092ca1cd67 | ||
|   | 0df2539641 | ||
|   | 0f2d8a599c | ||
|   | 54b3143a1d | ||
|   | 148f7d2a91 | ||
|   | 1aa662f763 | ||
|   | 0b86b88de7 | ||
|   | 98033b1ba7 | ||
|   | 2b7eab629d | ||
|   | 0e4973e15c | ||
|   | af0acf0dae | ||
|   | 76e5fe5a87 | ||
|   | 802c80f40c | ||
|   | a51c5bd905 | ||
|   | 8c68556f52 | ||
|   | cca1ea2404 | ||
|   | 281016a501 | ||
|   | d4acdf2f89 | ||
|   | 0951e75c85 | ||
|   | 6b017b226a | ||
|   | 9e3bd7398c | ||
|   | 79f764c7a8 | ||
|   | b5dc4353fb | ||
|   | 2fbac73c29 | ||
|   | 6616d105d1 | ||
|   | 6b4b19194e | ||
|   | 9785edd263 | ||
|   | 2a0bc11b68 | ||
|   | dd0325a88d | ||
|   | 20783c0978 | ||
|   | 3f06a40bd5 | ||
|   | 68f43985ad | ||
|   | 915ca8f817 | ||
|   | a65a81610b | ||
|   | 8eb6ed5639 | ||
|   | 795a8705c3 | ||
|   | 3af0dc3b3a | ||
|   | 9cf9b958a3 | ||
|   | 3ac2ba8d5a | ||
|   | d893421c7b | ||
|   | 250b3bb579 | ||
|   | e9edbfc051 | ||
|   | e343db6f72 | ||
|   | 4d57d66f85 | ||
|   | 54ed6320c2 | ||
|   | 23083f3ae0 | ||
|   | 1985873494 | ||
|   | 8ae5917659 | ||
|   | c91bfd08d8 | ||
|   | 49110a5872 | ||
|   | c01c8edeb8 | ||
|   | ff8cf067b8 | ||
|   | 1420f68050 | ||
|   | c0be3e585a | ||
|   | 3049ef9151 | ||
|   | 4be00bbe6b | ||
|   | 9382dde098 | ||
|   | 1bf46b7711 | ||
|   | b85bae31d9 | ||
|   | 0898829313 | ||
|   | f8ad877601 | ||
|   | 585d1556c1 | ||
|   | 7486555875 | ||
|   | fc30b1bacc | ||
|   | 0dd19af6e8 | ||
|   | 4c44515f9d | ||
|   | 9d84d6dd64 | ||
|   | 0f708daf2d | ||
|   | b9354de8fd | ||
|   | c9d5f4c898 | ||
|   | 810c150781 | ||
|   | 31dd538c0b | ||
|   | 62e38e7c45 | ||
|   | b9da28a29b | ||
|   | 84bfa8a6b1 | ||
|   | 1f830963f6 | ||
|   | 12d2c6fe89 | ||
|   | f43faf15f8 | ||
|   | 173a38a374 | ||
|   | 1604ff15b5 | 
							
								
								
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | Dockerfile | ||||||
|  | tgs.Dockerfile | ||||||
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/Bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/Bug_report.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,7 @@ | |||||||
| --- | --- | ||||||
| name: Bug report | name: Bug report | ||||||
| about: Create a report to help us improve. (Check the FAQ on the wiki first) | about: Create a report to help us improve. (Check the FAQ on the wiki first) | ||||||
|  | labels: bug | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/Feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/Feature_request.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,7 @@ | |||||||
| --- | --- | ||||||
| name: Feature request | name: Feature request | ||||||
| about: Suggest an idea for this project | about: Suggest an idea for this project | ||||||
|  | labels: enhancement | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | # Docs: <https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-dependency-updates> | ||||||
|  |  | ||||||
|  | version: 2 | ||||||
|  |  | ||||||
|  | updates: | ||||||
|  |   - package-ecosystem: gomod | ||||||
|  |     directory: / | ||||||
|  |     schedule: {interval: weekly} | ||||||
|  |     reviewers: [42wim] | ||||||
|  |     assignees: [42wim] | ||||||
|  |  | ||||||
|  |   - package-ecosystem: github-actions | ||||||
|  |     directory: / | ||||||
|  |     schedule: {interval: weekly} | ||||||
|  |     reviewers: [42wim] | ||||||
|  |     assignees: [42wim] | ||||||
|  |  | ||||||
|  |   - package-ecosystem: docker | ||||||
|  |     directory: / | ||||||
|  |     schedule: {interval: weekly} | ||||||
|  |     reviewers: [42wim] | ||||||
|  |     assignees: [42wim] | ||||||
							
								
								
									
										71
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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@v2 | ||||||
|  |         with: | ||||||
|  |           fetch-depth: 20 | ||||||
|  |       - name: Run golangci-lint | ||||||
|  |         uses: golangci/golangci-lint-action@v2 | ||||||
|  |         with: | ||||||
|  |           version: latest | ||||||
|  |           args: "-v --new-from-rev HEAD~5" | ||||||
|  |   test-build-upload: | ||||||
|  |     strategy: | ||||||
|  |       matrix: | ||||||
|  |         go-version: [1.17.x] | ||||||
|  |         platform: [ubuntu-latest] | ||||||
|  |     runs-on: ${{ matrix.platform }} | ||||||
|  |     steps: | ||||||
|  |     - name: Install Go | ||||||
|  |       uses: actions/setup-go@v2 | ||||||
|  |       with: | ||||||
|  |         go-version: ${{ matrix.go-version }} | ||||||
|  |         stable: false | ||||||
|  |     - name: Checkout code | ||||||
|  |       uses: actions/checkout@v2 | ||||||
|  |       with: | ||||||
|  |           fetch-depth: 0 | ||||||
|  |     - name: Test | ||||||
|  |       run: go test ./... -mod=vendor | ||||||
|  |     - name: Build | ||||||
|  |       run: | | ||||||
|  |         mkdir -p output/{win,lin,arm,mac} | ||||||
|  |         VERSION=$(git describe --tags) | ||||||
|  |         CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64 | ||||||
|  |         CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe | ||||||
|  |         CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64 | ||||||
|  |     - name: Upload linux 64-bit | ||||||
|  |       if: startsWith(matrix.go-version,'1.17') | ||||||
|  |       uses: actions/upload-artifact@v2 | ||||||
|  |       with: | ||||||
|  |         name: matterbridge-linux-64bit | ||||||
|  |         path: output/lin | ||||||
|  |     - name: Upload windows 64-bit | ||||||
|  |       if: startsWith(matrix.go-version,'1.17') | ||||||
|  |       uses: actions/upload-artifact@v2 | ||||||
|  |       with: | ||||||
|  |         name: matterbridge-windows-64bit | ||||||
|  |         path: output/win | ||||||
|  |     - name: Upload darwin 64-bit | ||||||
|  |       if: startsWith(matrix.go-version,'1.17') | ||||||
|  |       uses: actions/upload-artifact@v2 | ||||||
|  |       with: | ||||||
|  |         name: matterbridge-darwin-64bit | ||||||
|  |         path: output/mac | ||||||
							
								
								
									
										68
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										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 }} | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,9 @@ | |||||||
| # Exclude matterbridge binary | # Exclude matterbridge binary | ||||||
|  | /matterbridge | ||||||
|  | /matterbridge.exe | ||||||
|  |  | ||||||
| # Exclude configuration file | # Exclude configuration file | ||||||
| matterbridge.toml | matterbridge.toml | ||||||
|  |  | ||||||
|  | # Exclude IDE Files | ||||||
|  | .vscode | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ run: | |||||||
|   # concurrency: 4 |   # concurrency: 4 | ||||||
|  |  | ||||||
|   # timeout for analysis, e.g. 30s, 5m, default is 1m |   # timeout for analysis, e.g. 30s, 5m, default is 1m | ||||||
|   deadline: 2m |   deadline: 5m | ||||||
|  |  | ||||||
|   # exit code when at least one issue was found, default is 1 |   # exit code when at least one issue was found, default is 1 | ||||||
|   issues-exit-code: 1 |   issues-exit-code: 1 | ||||||
| @@ -23,7 +23,7 @@ run: | |||||||
|   # default value is empty list, but next dirs are always skipped independently |   # default value is empty list, but next dirs are always skipped independently | ||||||
|   # from this option's value: |   # from this option's value: | ||||||
|   #   	vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ |   #   	vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ | ||||||
|   skip-dirs: |   skip-dirs: gateway/bridgemap$ | ||||||
|  |  | ||||||
|   # which files to skip: they will be analyzed, but issues from them |   # which files to skip: they will be analyzed, but issues from them | ||||||
|   # won't be reported. Default value is empty list, but there is |   # won't be reported. Default value is empty list, but there is | ||||||
| @@ -91,7 +91,6 @@ linters-settings: | |||||||
|     # Correct spellings using locale preferences for US or UK. |     # Correct spellings using locale preferences for US or UK. | ||||||
|     # Default is to use a neutral variety of English. |     # Default is to use a neutral variety of English. | ||||||
|     # Setting locale to US will correct the British spelling of 'colour' to 'color'. |     # Setting locale to US will correct the British spelling of 'colour' to 'color'. | ||||||
|     locale: US |  | ||||||
|   lll: |   lll: | ||||||
|     # max line length, lines longer will be reported. Default is 120. |     # max line length, lines longer will be reported. Default is 120. | ||||||
|     # '\t' is counted as 1 character by default, and can be changed with the tab-width option |     # '\t' is counted as 1 character by default, and can be changed with the tab-width option | ||||||
| @@ -175,8 +174,36 @@ linters: | |||||||
|     - maligned |     - maligned | ||||||
|     - prealloc |     - prealloc | ||||||
|     - wsl |     - wsl | ||||||
|  |     - gomnd | ||||||
|  |     - godox | ||||||
|  |     - goerr113 | ||||||
|  |     - testpackage | ||||||
|  |     - godot | ||||||
|  |     - interfacer | ||||||
|  |     - goheader | ||||||
|  |     - noctx | ||||||
|  |     - gci | ||||||
|  |     - errorlint | ||||||
|  |     - nlreturn | ||||||
|  |     - exhaustivestruct | ||||||
|  |     - forbidigo | ||||||
|  |     - wrapcheck | ||||||
|  |     - varnamelen | ||||||
|  |     - ireturn | ||||||
|  |     - errorlint | ||||||
|  |     - tparallel | ||||||
|  |     - wrapcheck | ||||||
|  |     - paralleltest | ||||||
|  |     - makezero | ||||||
|  |     - thelper | ||||||
|  |     - cyclop | ||||||
|  |     - revive | ||||||
|  |     - importas | ||||||
|  |     - gomoddirectives | ||||||
|  |     - promlinter | ||||||
|  |     - tagliatelle | ||||||
|  |     - errname | ||||||
|  |     - typecheck | ||||||
| # rules to deal with reported isues | # rules to deal with reported isues | ||||||
| issues: | issues: | ||||||
|   # List of regexps of issue texts to exclude, empty list by default. |   # List of regexps of issue texts to exclude, empty list by default. | ||||||
|   | |||||||
| @@ -18,8 +18,11 @@ builds: | |||||||
|     - arm |     - arm | ||||||
|     - arm64 |     - arm64 | ||||||
|     - 386 |     - 386 | ||||||
|  |   goarm: | ||||||
|  |     - 6 | ||||||
|  |     - 7 | ||||||
|   ldflags: |   ldflags: | ||||||
|     - -s -w -X main.githash={{.ShortCommit}} |     - -s -w -X github.com/42wim/matterbridge/version.GitHash={{.ShortCommit}} | ||||||
|  |  | ||||||
| archives: | archives: | ||||||
|   - |   - | ||||||
|   | |||||||
							
								
								
									
										56
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										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.13.x |  | ||||||
|     env: |  | ||||||
|     - GO111MODULE=on |  | ||||||
|     - GOLANGCI_VERSION="v1.21.0" |  | ||||||
|   - stage: test |  | ||||||
|     # Run tests in a combination of Go environments. |  | ||||||
|     script: ./ci/test.sh |  | ||||||
|     go: 1.12.x |  | ||||||
|     env: |  | ||||||
|     - GO111MODULE=off |  | ||||||
|   - script: ./ci/test.sh |  | ||||||
|     go: 1.12.x |  | ||||||
|     env: |  | ||||||
|     - GO111MODULE=on |  | ||||||
|   - script: ./ci/test.sh |  | ||||||
|     go: 1.13.x |  | ||||||
|     env: |  | ||||||
|     - GO111MODULE=on |  | ||||||
|     - REPORT_COVERAGE=1 |  | ||||||
|     - BINDEPLOY=1 |  | ||||||
|  |  | ||||||
| before_deploy: /bin/bash ci/bintray.sh |  | ||||||
|  |  | ||||||
| deploy: |  | ||||||
|   on: |  | ||||||
|     all_branches: true |  | ||||||
|     condition: $BINDEPLOY = 1 |  | ||||||
|   provider: bintray |  | ||||||
|   edge: |  | ||||||
|     branch: v1.8.47 |  | ||||||
|   file: ci/deploy.json |  | ||||||
|   user: 42wim |  | ||||||
|   key: |  | ||||||
|     secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI=" |  | ||||||
							
								
								
									
										23
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,11 +1,14 @@ | |||||||
| FROM alpine:edge | FROM alpine AS builder | ||||||
| ENTRYPOINT ["/bin/matterbridge"] |  | ||||||
|  |  | ||||||
| COPY . /go/src/github.com/42wim/matterbridge | COPY . /go/src/matterbridge | ||||||
| RUN apk update && apk add go git gcc musl-dev ca-certificates \ | RUN apk --no-cache add go git \ | ||||||
|         && cd /go/src/github.com/42wim/matterbridge \ |         && cd /go/src/matterbridge \ | ||||||
|         && export GOPATH=/go \ |         && 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 | ||||||
|         && go get \ |  | ||||||
|         && go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge \ | FROM alpine | ||||||
|         && rm -rf /go \ | RUN apk --no-cache add ca-certificates mailcap | ||||||
|         && apk del --purge git go gcc musl-dev | 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"] | ||||||
|   | |||||||
							
								
								
									
										237
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										237
									
								
								README.md
									
									
									
									
									
								
							| @@ -9,26 +9,26 @@ Letting people be where they want to be.<br /> | |||||||
|  |  | ||||||
|    <sup> |    <sup> | ||||||
|  |  | ||||||
|  | [Discord][mb-discord] | | ||||||
| [Gitter][mb-gitter] | | [Gitter][mb-gitter] | | ||||||
| [IRC][mb-irc] | | [IRC][mb-irc] | | ||||||
| [Discord][mb-discord] | | [Keybase][mb-keybase] | | ||||||
| [Matrix][mb-matrix] | | [Matrix][mb-matrix] | | ||||||
| [Slack][mb-slack] | |  | ||||||
| [Mattermost][mb-mattermost] | | [Mattermost][mb-mattermost] | | ||||||
|  | [MSTeams][mb-msteams] | | ||||||
| [Rocket.Chat][mb-rocketchat] | | [Rocket.Chat][mb-rocketchat] | | ||||||
| [XMPP][mb-xmpp] | | [Slack][mb-slack] | | ||||||
|  | [Telegram][mb-telegram] | | ||||||
| [Twitch][mb-twitch] | | [Twitch][mb-twitch] | | ||||||
| [WhatsApp][mb-whatsapp] | | [WhatsApp][mb-whatsapp] | | ||||||
|  | [XMPP][mb-xmpp] | | ||||||
| [Zulip][mb-zulip] | | [Zulip][mb-zulip] | | ||||||
| [Telegram][mb-telegram] | |  | ||||||
| [Keybase][mb-keybase] | |  | ||||||
| And more... | And more... | ||||||
| </sup> | </sup> | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| [](https://github.com/42wim/matterbridge/releases/latest) | [](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/maintainability) | ||||||
| [](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br /> | [](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br /> | ||||||
|  |  | ||||||
| @@ -44,28 +44,35 @@ And more... | |||||||
|   </a> |   </a> | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
| ### Table of Contents | # Table of Contents | ||||||
|  |  | ||||||
| - [Features](https://github.com/42wim/matterbridge/wiki/Features) | - [matterbridge](#matterbridge) | ||||||
|   - [Natively supported](#natively-supported) | - [Table of Contents](#table-of-contents) | ||||||
|   - [3rd party via matterbridge api](#3rd-party-via-matterbridge-api) |   - [Features](#features) | ||||||
|   - [API](#API) |     - [Natively supported](#natively-supported) | ||||||
| - [Chat with us](#chat-with-us) |     - [3rd party via matterbridge api](#3rd-party-via-matterbridge-api) | ||||||
| - [Screenshots](https://github.com/42wim/matterbridge/wiki/) |     - [API](#api) | ||||||
| - [Installing/upgrading](#installing--upgrading) |   - [Chat with us](#chat-with-us) | ||||||
|   - [Binaries](#binaries) |   - [Screenshots](#screenshots) | ||||||
| - [Building](#building) |   - [Installing / upgrading](#installing--upgrading) | ||||||
| - [Configuration](#configuration) |     - [Binaries](#binaries) | ||||||
|   - [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) |     - [Packages](#packages) | ||||||
|   - [Settings](#settings) |   - [Building](#building) | ||||||
|   - [Examples](#examples) |   - [Configuration](#configuration) | ||||||
| - [Running](#running) |     - [Basic configuration](#basic-configuration) | ||||||
|   - [Docker](#docker) |     - [Settings](#settings) | ||||||
| - [Changelog](#changelog) |     - [Advanced configuration](#advanced-configuration) | ||||||
| - [FAQ](#faq) |     - [Examples](#examples) | ||||||
| - [Related projects](#related-projects) |       - [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing) | ||||||
| - [Articles](#articles) |       - [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general) | ||||||
| - [Thanks](#thanks) |   - [Running](#running) | ||||||
|  |     - [Docker](#docker) | ||||||
|  |     - [Systemd](#systemd) | ||||||
|  |   - [Changelog](#changelog) | ||||||
|  |   - [FAQ](#faq) | ||||||
|  |   - [Related projects](#related-projects) | ||||||
|  |   - [Articles / Tutorials](#articles--tutorials) | ||||||
|  |   - [Thanks](#thanks) | ||||||
|  |  | ||||||
| ## Features | ## Features | ||||||
|  |  | ||||||
| @@ -80,29 +87,39 @@ And more... | |||||||
|  |  | ||||||
| ### Natively supported | ### 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) | - [Discord](https://discordapp.com) | ||||||
| - [Telegram](https://telegram.org) | - [Gitter](https://gitter.im) | ||||||
| - [Rocket.chat](https://rocket.chat) | - [IRC](http://www.mirc.com/servers.html) | ||||||
| - [Matrix](https://matrix.org) |  | ||||||
| - [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) |  | ||||||
| - [Keybase](https://keybase.io) | - [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/) | ||||||
|  | - [XMPP](https://xmpp.org) | ||||||
|  | - [Zulip](https://zulipchat.com) | ||||||
|  |  | ||||||
| ### 3rd party via matterbridge api | ### 3rd party via matterbridge api | ||||||
|  |  | ||||||
| - [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) | - [Discourse](https://github.com/DeclanHoare/matterbabble) | ||||||
|  | - [Facebook messenger](https://github.com/powerjungle/fbridge-asyncio) | ||||||
|  | - [Facebook messenger](https://github.com/VictorNine/fbridge) | ||||||
|  | - [Minecraft](https://github.com/elytra/MatterLink) | ||||||
|  | - [Minecraft](https://github.com/raws/mattercraft) | ||||||
|  | - [Minecraft](https://gitlab.com/Programie/MatterBukkit) | ||||||
|  | - [Reddit](https://github.com/bonehurtingjuice/mattereddit) | ||||||
| - [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430) | - [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430) | ||||||
|  | - [MatterAMXX](https://github.com/GabeIggy/MatterAMXX) | ||||||
|  | - [Vintage Story](https://github.com/NikkyAI/vs-matterbridge) | ||||||
|  |  | ||||||
| ### API | ### API | ||||||
|  |  | ||||||
| @@ -111,61 +128,75 @@ 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. | 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) | - [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot) | ||||||
| - [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support) | - [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support) | ||||||
|  | - [fbridge-asyncio](https://github.com/powerjungle/fbridge-asyncio) (Facebook messenger support) | ||||||
| - [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) | - [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) | ||||||
| - [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support) | - [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support) | ||||||
| - [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod) | - [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod) | ||||||
|  | - [Vintage Story](https://github.com/NikkyAI/vs-matterbridge) | ||||||
|  |  | ||||||
| ## Chat with us | ## Chat with us | ||||||
|  |  | ||||||
| Questions or want to test on your favorite platform? Join below: | Questions or want to test on your favorite platform? Join below: | ||||||
|  |  | ||||||
|  | - [Discord][mb-discord] | ||||||
| - [Gitter][mb-gitter] | - [Gitter][mb-gitter] | ||||||
| - [IRC][mb-irc] | - [IRC][mb-irc] | ||||||
| - [Discord][mb-discord] | - [Keybase][mb-keybase] | ||||||
| - [Matrix][mb-matrix] | - [Matrix][mb-matrix] | ||||||
| - [Slack][mb-slack] |  | ||||||
| - [Mattermost][mb-mattermost] | - [Mattermost][mb-mattermost] | ||||||
| - [Rocket.Chat][mb-rocketchat] | - [Rocket.Chat][mb-rocketchat] | ||||||
| - [XMPP][mb-xmpp] (matterbridge@conference.jabber.de) | - [Slack][mb-slack] | ||||||
| - [Twitch][mb-twitch] |  | ||||||
| - [Zulip][mb-zulip] |  | ||||||
| - [Telegram][mb-telegram] | - [Telegram][mb-telegram] | ||||||
|  | - [Twitch][mb-twitch] | ||||||
|  | - [XMPP][mb-xmpp] (matterbridge@conference.jabber.de) | ||||||
|  | - [Zulip][mb-zulip] | ||||||
|  |  | ||||||
| ## Screenshots | ## Screenshots | ||||||
|  |  | ||||||
| See https://github.com/42wim/matterbridge/wiki | See <https://github.com/42wim/matterbridge/wiki> | ||||||
|  |  | ||||||
| ## Installing / upgrading | ## Installing / upgrading | ||||||
|  |  | ||||||
| ### Binaries | ### Binaries | ||||||
|  |  | ||||||
| - Latest stable release [v1.16.2](https://github.com/42wim/matterbridge/releases/latest) | - Latest stable release [v1.23.2](https://github.com/42wim/matterbridge/releases/latest) | ||||||
| - Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/) | - Development releases (follows master) can be downloaded [here](https://github.com/42wim/matterbridge/actions) selecting the latest green build and then artifacts. | ||||||
|  |  | ||||||
| To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest) 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.20.0-linux-64bit`). After downloading (and making the binary executable, if necessary), follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. | ||||||
|  |  | ||||||
| ### Packages | ### Packages | ||||||
|  |  | ||||||
| - [Overview](https://repology.org/metapackage/matterbridge/versions) | - [Overview](https://repology.org/metapackage/matterbridge/versions) | ||||||
|  | - [snap](https://snapcraft.io/matterbridge) | ||||||
|  | - [scoop](https://github.com/42wim/scoop-bucket) | ||||||
|  |  | ||||||
| ## Building | ## Building | ||||||
|  |  | ||||||
| Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest) | Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest) | ||||||
|  |  | ||||||
| If you really want to build from source, follow these instructions: | If you really want to build from source, follow these instructions: | ||||||
| Go 1.12+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed. | Go 1.17+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed. | ||||||
|  |  | ||||||
|  | To install the latest stable run: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | go install github.com/42wim/matterbridge | ||||||
| ``` | ``` | ||||||
| go get github.com/42wim/matterbridge |  | ||||||
|  | To install the latest dev run: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | go install github.com/42wim/matterbridge@master | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| You should now have matterbridge binary in the ~/go/bin directory: | You should now have matterbridge binary in the ~/go/bin directory: | ||||||
|  |  | ||||||
| ``` | ```bash | ||||||
| $ ls ~/go/bin/ | $ ls ~/go/bin/ | ||||||
| matterbridge | matterbridge | ||||||
| ``` | ``` | ||||||
| @@ -190,8 +221,8 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for | |||||||
|  |  | ||||||
| ```toml | ```toml | ||||||
| [irc] | [irc] | ||||||
|     [irc.freenode] |     [irc.libera] | ||||||
|     Server="irc.freenode.net:6667" |     Server="irc.libera.chat:6667" | ||||||
|     Nick="yourbotname" |     Nick="yourbotname" | ||||||
|  |  | ||||||
| [mattermost] | [mattermost] | ||||||
| @@ -207,7 +238,7 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for | |||||||
| name="mygateway" | name="mygateway" | ||||||
| enable=true | enable=true | ||||||
|     [[gateway.inout]] |     [[gateway.inout]] | ||||||
|     account="irc.freenode" |     account="irc.libera" | ||||||
|     channel="#testing" |     channel="#testing" | ||||||
|  |  | ||||||
|     [[gateway.inout]] |     [[gateway.inout]] | ||||||
| @@ -248,7 +279,7 @@ RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> " | |||||||
|  |  | ||||||
| See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. | See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. | ||||||
|  |  | ||||||
| ``` | ```bash | ||||||
| Usage of ./matterbridge: | Usage of ./matterbridge: | ||||||
|   -conf string |   -conf string | ||||||
|         config file (default "matterbridge.toml") |         config file (default "matterbridge.toml") | ||||||
| @@ -262,11 +293,11 @@ Usage of ./matterbridge: | |||||||
|  |  | ||||||
| ### Docker | ### Docker | ||||||
|  |  | ||||||
| Create your matterbridge.toml file locally eg in `/tmp/matterbridge.toml` | Please take a look at the [Docker Wiki page](https://github.com/42wim/matterbridge/wiki/Deploy:-Docker) for more information. | ||||||
|  |  | ||||||
| ``` | ### Systemd | ||||||
| docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge |  | ||||||
| ``` | Please take a look at the [Service Files page](https://github.com/42wim/matterbridge/wiki/Service-files) for more information. | ||||||
|  |  | ||||||
| ## Changelog | ## Changelog | ||||||
|  |  | ||||||
| @@ -278,7 +309,7 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) | |||||||
|  |  | ||||||
| ## Related projects | ## Related projects | ||||||
|  |  | ||||||
| - [FOSSRIT/infrastructure - roles/matterbridge](https://github.com/FOSSRIT/infrastructure/tree/master/roles/matterbridge) (Ansible role used to automate deployments of Matterbridge) | - [jwflory/ansible-role-matterbridge](https://galaxy.ansible.com/jwflory/matterbridge) (Ansible role to simplify deploying Matterbridge) | ||||||
| - [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig) | - [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig) | ||||||
| - [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer) | - [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer) | ||||||
| - [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku) | - [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku) | ||||||
| @@ -289,18 +320,24 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) | |||||||
| - [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) | - [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) | ||||||
| - [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge) | - [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge) | ||||||
| - [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge) | - [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge) | ||||||
|  | - [nextcloud talk](https://github.com/nextcloud/talk_matterbridge) (Integrates matterbridge in Nextcloud Talk) | ||||||
|  | - [mattercraft](https://github.com/raws/mattercraft) (Minecraft bridge) | ||||||
|  | - [vs-matterbridge](https://github.com/NikkyAI/vs-matterbridge) (Vintage Story bridge) | ||||||
|  |  | ||||||
| ## Articles | ## 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) | - [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3) | ||||||
| - https://mattermost.com/blog/connect-irc-to-mattermost/ | - <https://mattermost.com/blog/connect-irc-to-mattermost/> | ||||||
| - https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/ | - <https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/> | ||||||
| - https://blog.brightscout.com/top-10-mattermost-integrations/ | - <https://blog.brightscout.com/top-10-mattermost-integrations/> | ||||||
| - http://bencey.co.nz/2018/09/17/bridge/ | - <https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/> | ||||||
| - https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/ | - <https://kopano.com/blog/matterbridge-bridging-mattermost-chat/> | ||||||
| - https://kopano.com/blog/matterbridge-bridging-mattermost-chat/ | - <https://www.stitcher.com/s/?eid=52382713> | ||||||
| - https://www.stitcher.com/s/?eid=52382713 | - <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/> | ||||||
| - https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/ | - <https://userlinux.net/mattermost-and-matterbridge.html> | ||||||
|  | - <https://nextcloud.com/blog/bridging-chat-services-in-talk/> | ||||||
|  | - <https://minecraftchest1.wordpress.com/2021/06/05/how-to-install-and-setup-matterbridge/> | ||||||
|  | - Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc) | ||||||
|  |  | ||||||
| ## Thanks | ## Thanks | ||||||
|  |  | ||||||
| @@ -313,36 +350,42 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) | |||||||
|  |  | ||||||
| Matterbridge wouldn't exist without these libraries: | Matterbridge wouldn't exist without these libraries: | ||||||
|  |  | ||||||
| - discord - https://github.com/bwmarrin/discordgo | - discord - <https://github.com/bwmarrin/discordgo> | ||||||
| - echo - https://github.com/labstack/echo | - echo - <https://github.com/labstack/echo> | ||||||
| - gitter - https://github.com/sromku/go-gitter | - gitter - <https://github.com/sromku/go-gitter> | ||||||
| - gops - https://github.com/google/gops | - gops - <https://github.com/google/gops> | ||||||
| - gozulipbot - https://github.com/ifo/gozulipbot | - gozulipbot - <https://github.com/ifo/gozulipbot> | ||||||
| - irc - https://github.com/lrstanley/girc | - gumble - <https://github.com/layeh/gumble> | ||||||
| - mattermost - https://github.com/mattermost/mattermost-server | - irc - <https://github.com/lrstanley/girc> | ||||||
| - matrix - https://github.com/matrix-org/gomatrix | - keybase - <https://github.com/keybase/go-keybase-chat-bot> | ||||||
| - sshchat - https://github.com/shazow/ssh-chat | - matrix - <https://github.com/matrix-org/gomatrix> | ||||||
| - slack - https://github.com/nlopes/slack | - mattermost - <https://github.com/mattermost/mattermost-server> | ||||||
| - steam - https://github.com/Philipp15b/go-steam | - msgraph.go - <https://github.com/yaegashi/msgraph.go> | ||||||
| - telegram - https://github.com/go-telegram-bot-api/telegram-bot-api | - mumble - <https://github.com/layeh/gumble> | ||||||
| - xmpp - https://github.com/mattn/go-xmpp | - nctalk - <https://github.com/gary-kim/go-nc-talk> | ||||||
| - whatsapp - https://github.com/Rhymen/go-whatsapp/ | - slack - <https://github.com/nlopes/slack> | ||||||
| - zulip - https://github.com/ifo/gozulipbot | - sshchat - <https://github.com/shazow/ssh-chat> | ||||||
| - tengo - https://github.com/d5/tengo | - steam - <https://github.com/Philipp15b/go-steam> | ||||||
| - keybase - https://github.com/keybase/go-keybase-chat-bot | - telegram - <https://github.com/go-telegram-bot-api/telegram-bot-api> | ||||||
|  | - tengo - <https://github.com/d5/tengo> | ||||||
|  | - vk - <https://github.com/SevereCloud/vksdk> | ||||||
|  | - whatsapp - <https://github.com/Rhymen/go-whatsapp> | ||||||
|  | - xmpp - <https://github.com/mattn/go-xmpp> | ||||||
|  | - zulip - <https://github.com/ifo/gozulipbot> | ||||||
|  |  | ||||||
| <!-- Links --> | <!-- Links --> | ||||||
|  |  | ||||||
| [mb-gitter]: https://gitter.im/42wim/matterbridge |  | ||||||
| [mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat |  | ||||||
| [mb-discord]: https://discord.gg/AkKPtrQ | [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-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org | ||||||
| [mb-slack]: https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA |  | ||||||
| [mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e | [mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e | ||||||
|  | [mb-msteams]: https://teams.microsoft.com/join/hj92x75gd3y7 | ||||||
| [mb-rocketchat]: https://open.rocket.chat/channel/matterbridge | [mb-rocketchat]: https://open.rocket.chat/channel/matterbridge | ||||||
| [mb-xmpp]: https://inverse.chat/ | [mb-slack]: https://join.slack.com/t/matterbridgechat/shared_invite/zt-2ourq2h2-7YvyYBq2WFGC~~zEzA68_Q | ||||||
|  | [mb-telegram]: https://t.me/Matterbridge | ||||||
| [mb-twitch]: https://www.twitch.tv/matterbridge | [mb-twitch]: https://www.twitch.tv/matterbridge | ||||||
| [mb-whatsapp]: https://www.whatsapp.com/ | [mb-whatsapp]: https://www.whatsapp.com/ | ||||||
| [mb-keybase]: https://keybase.io | [mb-xmpp]: https://inverse.chat/ | ||||||
| [mb-zulip]: https://matterbridge.zulipchat.com/register/ | [mb-zulip]: https://matterbridge.zulipchat.com/register/ | ||||||
| [mb-telegram]: https://t.me/Matterbridge |  | ||||||
|   | |||||||
| @@ -6,17 +6,20 @@ import ( | |||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"gopkg.in/olahol/melody.v1" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/labstack/echo/v4" | 	"github.com/labstack/echo/v4" | ||||||
| 	"github.com/labstack/echo/v4/middleware" | 	"github.com/labstack/echo/v4/middleware" | ||||||
| 	"github.com/zfjagann/golang-ring" | 	ring "github.com/zfjagann/golang-ring" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type API struct { | type API struct { | ||||||
| 	Messages ring.Ring | 	Messages ring.Ring | ||||||
| 	sync.RWMutex | 	sync.RWMutex | ||||||
| 	*bridge.Config | 	*bridge.Config | ||||||
|  | 	mrouter *melody.Melody | ||||||
| } | } | ||||||
|  |  | ||||||
| type Message struct { | type Message struct { | ||||||
| @@ -32,6 +35,32 @@ func New(cfg *bridge.Config) bridge.Bridger { | |||||||
| 	e := echo.New() | 	e := echo.New() | ||||||
| 	e.HideBanner = true | 	e.HideBanner = true | ||||||
| 	e.HidePort = true | 	e.HidePort = true | ||||||
|  |  | ||||||
|  | 	b.mrouter = melody.New() | ||||||
|  | 	b.mrouter.HandleMessage(func(s *melody.Session, msg []byte) { | ||||||
|  | 		message := config.Message{} | ||||||
|  | 		err := json.Unmarshal(msg, &message) | ||||||
|  | 		if err != nil { | ||||||
|  | 			b.Log.Errorf("failed to decode message from byte[] '%s'", string(msg)) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		b.handleWebsocketMessage(message, s) | ||||||
|  | 	}) | ||||||
|  | 	b.mrouter.HandleConnect(func(session *melody.Session) { | ||||||
|  | 		greet := b.getGreeting() | ||||||
|  | 		data, err := json.Marshal(greet) | ||||||
|  | 		if err != nil { | ||||||
|  | 			b.Log.Errorf("failed to encode message '%v'", greet) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		err = session.Write(data) | ||||||
|  | 		if err != nil { | ||||||
|  | 			b.Log.Errorf("failed to write message '%s'", string(data)) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		// TODO: send message history buffer from `b.Messages` here | ||||||
|  | 	}) | ||||||
|  |  | ||||||
| 	b.Messages = ring.Ring{} | 	b.Messages = ring.Ring{} | ||||||
| 	if b.GetInt("Buffer") != 0 { | 	if b.GetInt("Buffer") != 0 { | ||||||
| 		b.Messages.SetCapacity(b.GetInt("Buffer")) | 		b.Messages.SetCapacity(b.GetInt("Buffer")) | ||||||
| @@ -41,9 +70,17 @@ func New(cfg *bridge.Config) bridge.Bridger { | |||||||
| 			return key == b.GetString("Token"), nil | 			return key == b.GetString("Token"), nil | ||||||
| 		})) | 		})) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Set RemoteNickFormat to a sane default | ||||||
|  | 	if !b.IsKeySet("RemoteNickFormat") { | ||||||
|  | 		b.Log.Debugln("RemoteNickFormat is unset, defaulting to \"{NICK}\"") | ||||||
|  | 		b.Config.Config.Viper().Set(b.GetConfigKey("RemoteNickFormat"), "{NICK}") | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	e.GET("/api/health", b.handleHealthcheck) | 	e.GET("/api/health", b.handleHealthcheck) | ||||||
| 	e.GET("/api/messages", b.handleMessages) | 	e.GET("/api/messages", b.handleMessages) | ||||||
| 	e.GET("/api/stream", b.handleStream) | 	e.GET("/api/stream", b.handleStream) | ||||||
|  | 	e.GET("/api/websocket", b.handleWebsocket) | ||||||
| 	e.POST("/api/message", b.handlePostMessage) | 	e.POST("/api/message", b.handlePostMessage) | ||||||
| 	go func() { | 	go func() { | ||||||
| 		if b.GetString("BindAddress") == "" { | 		if b.GetString("BindAddress") == "" { | ||||||
| @@ -58,13 +95,13 @@ func New(cfg *bridge.Config) bridge.Bridger { | |||||||
| func (b *API) Connect() error { | func (b *API) Connect() error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *API) Disconnect() error { | func (b *API) Disconnect() error { | ||||||
| 	return nil | 	return nil | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *API) JoinChannel(channel config.ChannelInfo) error { | func (b *API) JoinChannel(channel config.ChannelInfo) error { | ||||||
| 	return nil | 	return nil | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *API) Send(msg config.Message) (string, error) { | func (b *API) Send(msg config.Message) (string, error) { | ||||||
| @@ -74,7 +111,14 @@ func (b *API) Send(msg config.Message) (string, error) { | |||||||
| 	if msg.Event == config.EventMsgDelete { | 	if msg.Event == config.EventMsgDelete { | ||||||
| 		return "", nil | 		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 | 	return "", nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -106,18 +150,23 @@ func (b *API) handleMessages(c echo.Context) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *API) handleStream(c echo.Context) error { | func (b *API) getGreeting() config.Message { | ||||||
| 	c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) | 	return config.Message{ | ||||||
| 	c.Response().WriteHeader(http.StatusOK) |  | ||||||
| 	greet := config.Message{ |  | ||||||
| 		Event:     config.EventAPIConnected, | 		Event:     config.EventAPIConnected, | ||||||
| 		Timestamp: time.Now(), | 		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 { | 	if err := json.NewEncoder(c.Response()).Encode(greet); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	c.Response().Flush() | 	c.Response().Flush() | ||||||
| 	for { | 	for { | ||||||
|  | 		// TODO: this causes issues, messages should be broadcasted to all connected clients | ||||||
| 		msg := b.Messages.Dequeue() | 		msg := b.Messages.Dequeue() | ||||||
| 		if msg != nil { | 		if msg != nil { | ||||||
| 			if err := json.NewEncoder(c.Response()).Encode(msg); err != nil { | 			if err := json.NewEncoder(c.Response()).Encode(msg); err != nil { | ||||||
| @@ -128,3 +177,31 @@ func (b *API) handleStream(c echo.Context) error { | |||||||
| 		time.Sleep(200 * time.Millisecond) | 		time.Sleep(200 * time.Millisecond) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (b *API) handleWebsocketMessage(message config.Message, s *melody.Session) { | ||||||
|  | 	message.Channel = "api" | ||||||
|  | 	message.Protocol = "api" | ||||||
|  | 	message.Account = b.Account | ||||||
|  | 	message.ID = "" | ||||||
|  | 	message.Timestamp = time.Now() | ||||||
|  |  | ||||||
|  | 	data, err := json.Marshal(message) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("failed to encode message for loopback '%v'", message) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	_ = b.mrouter.BroadcastOthers(data, s) | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("Sending websocket message from %s on %s to gateway", message.Username, "api") | ||||||
|  | 	b.Remote <- message | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *API) handleWebsocket(c echo.Context) error { | ||||||
|  | 	err := b.mrouter.HandleRequest(c.Response(), c.Request()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("error in websocket handling  '%v'", err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| package bridge | package bridge | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"log" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| @@ -41,6 +43,10 @@ type Factory func(*Config) Bridger | |||||||
|  |  | ||||||
| func New(bridge *config.Bridge) *Bridge { | func New(bridge *config.Bridge) *Bridge { | ||||||
| 	accInfo := strings.Split(bridge.Account, ".") | 	accInfo := strings.Split(bridge.Account, ".") | ||||||
|  | 	if len(accInfo) != 2 { | ||||||
|  | 		log.Fatalf("config failure, account incorrect: %s", bridge.Account) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	protocol := accInfo[0] | 	protocol := accInfo[0] | ||||||
| 	name := accInfo[1] | 	name := accInfo[1] | ||||||
|  |  | ||||||
| @@ -69,6 +75,7 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map | |||||||
| 	for ID, channel := range channels { | 	for ID, channel := range channels { | ||||||
| 		if !exists[ID] { | 		if !exists[ID] { | ||||||
| 			b.Log.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID) | 			b.Log.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID) | ||||||
|  | 			time.Sleep(time.Duration(b.GetInt("JoinDelay")) * time.Millisecond) | ||||||
| 			err := b.JoinChannel(channel) | 			err := b.JoinChannel(channel) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| @@ -79,8 +86,16 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (b *Bridge) GetConfigKey(key string) string { | ||||||
|  | 	return b.Account + "." + key | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bridge) IsKeySet(key string) bool { | ||||||
|  | 	return b.Config.IsKeySet(b.GetConfigKey(key)) || b.Config.IsKeySet("general."+key) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (b *Bridge) GetBool(key string) bool { | func (b *Bridge) GetBool(key string) bool { | ||||||
| 	val, ok := b.Config.GetBool(b.Account + "." + key) | 	val, ok := b.Config.GetBool(b.GetConfigKey(key)) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		val, _ = b.Config.GetBool("general." + key) | 		val, _ = b.Config.GetBool("general." + key) | ||||||
| 	} | 	} | ||||||
| @@ -88,7 +103,7 @@ func (b *Bridge) GetBool(key string) bool { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bridge) GetInt(key string) int { | func (b *Bridge) GetInt(key string) int { | ||||||
| 	val, ok := b.Config.GetInt(b.Account + "." + key) | 	val, ok := b.Config.GetInt(b.GetConfigKey(key)) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		val, _ = b.Config.GetInt("general." + key) | 		val, _ = b.Config.GetInt("general." + key) | ||||||
| 	} | 	} | ||||||
| @@ -96,7 +111,7 @@ func (b *Bridge) GetInt(key string) int { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bridge) GetString(key string) string { | func (b *Bridge) GetString(key string) string { | ||||||
| 	val, ok := b.Config.GetString(b.Account + "." + key) | 	val, ok := b.Config.GetString(b.GetConfigKey(key)) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		val, _ = b.Config.GetString("general." + key) | 		val, _ = b.Config.GetString("general." + key) | ||||||
| 	} | 	} | ||||||
| @@ -104,7 +119,7 @@ func (b *Bridge) GetString(key string) string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bridge) GetStringSlice(key string) []string { | func (b *Bridge) GetStringSlice(key string) []string { | ||||||
| 	val, ok := b.Config.GetStringSlice(b.Account + "." + key) | 	val, ok := b.Config.GetStringSlice(b.GetConfigKey(key)) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		val, _ = b.Config.GetStringSlice("general." + key) | 		val, _ = b.Config.GetStringSlice("general." + key) | ||||||
| 	} | 	} | ||||||
| @@ -112,7 +127,7 @@ func (b *Bridge) GetStringSlice(key string) []string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bridge) GetStringSlice2D(key string) [][]string { | func (b *Bridge) GetStringSlice2D(key string) [][]string { | ||||||
| 	val, ok := b.Config.GetStringSlice2D(b.Account + "." + key) | 	val, ok := b.Config.GetStringSlice2D(b.GetConfigKey(key)) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		val, _ = b.Config.GetStringSlice2D("general." + key) | 		val, _ = b.Config.GetStringSlice2D("general." + key) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ package config | |||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -21,11 +23,15 @@ const ( | |||||||
| 	EventRejoinChannels    = "rejoin_channels" | 	EventRejoinChannels    = "rejoin_channels" | ||||||
| 	EventUserAction        = "user_action" | 	EventUserAction        = "user_action" | ||||||
| 	EventMsgDelete         = "msg_delete" | 	EventMsgDelete         = "msg_delete" | ||||||
|  | 	EventFileDelete        = "file_delete" | ||||||
| 	EventAPIConnected      = "api_connected" | 	EventAPIConnected      = "api_connected" | ||||||
| 	EventUserTyping        = "user_typing" | 	EventUserTyping        = "user_typing" | ||||||
| 	EventGetChannelMembers = "get_channel_members" | 	EventGetChannelMembers = "get_channel_members" | ||||||
|  | 	EventNoticeIRC         = "notice_irc" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const ParentIDNotFound = "msg-parent-not-found" | ||||||
|  |  | ||||||
| type Message struct { | type Message struct { | ||||||
| 	Text      string    `json:"text"` | 	Text      string    `json:"text"` | ||||||
| 	Channel   string    `json:"channel"` | 	Channel   string    `json:"channel"` | ||||||
| @@ -42,14 +48,23 @@ type Message struct { | |||||||
| 	Extra     map[string][]interface{} | 	Extra     map[string][]interface{} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (m Message) ParentNotFound() bool { | ||||||
|  | 	return m.ParentID == ParentIDNotFound | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m Message) ParentValid() bool { | ||||||
|  | 	return m.ParentID != "" && !m.ParentNotFound() | ||||||
|  | } | ||||||
|  |  | ||||||
| type FileInfo struct { | type FileInfo struct { | ||||||
| 	Name    string | 	Name     string | ||||||
| 	Data    *[]byte | 	Data     *[]byte | ||||||
| 	Comment string | 	Comment  string | ||||||
| 	URL     string | 	URL      string | ||||||
| 	Size    int64 | 	Size     int64 | ||||||
| 	Avatar  bool | 	Avatar   bool | ||||||
| 	SHA     string | 	SHA      string | ||||||
|  | 	NativeID string | ||||||
| } | } | ||||||
|  |  | ||||||
| type ChannelInfo struct { | type ChannelInfo struct { | ||||||
| @@ -72,27 +87,34 @@ type ChannelMember struct { | |||||||
| type ChannelMembers []ChannelMember | type ChannelMembers []ChannelMember | ||||||
|  |  | ||||||
| type Protocol struct { | type Protocol struct { | ||||||
| 	AuthCode               string // steam | 	AllowMention           []string // discord | ||||||
| 	BindAddress            string // mattermost, slack // DEPRECATED | 	AuthCode               string   // steam | ||||||
| 	Buffer                 int    // api | 	BindAddress            string   // mattermost, slack // DEPRECATED | ||||||
| 	Charset                string // irc | 	Buffer                 int      // api | ||||||
| 	ColorNicks             bool   // only irc for now | 	Charset                string   // irc | ||||||
| 	Debug                  bool   // general | 	ClientID               string   // msteams | ||||||
| 	DebugLevel             int    // only for irc now | 	ColorNicks             bool     // only irc for now | ||||||
| 	EditSuffix             string // mattermost, slack, discord, telegram, gitter | 	Debug                  bool     // general | ||||||
| 	EditDisable            bool   // mattermost, slack, discord, telegram, gitter | 	DebugLevel             int      // only for irc now | ||||||
| 	IconURL                string // mattermost, slack | 	DisableWebPagePreview  bool     // telegram | ||||||
| 	IgnoreFailureOnStart   bool   // general | 	EditSuffix             string   // mattermost, slack, discord, telegram, gitter | ||||||
| 	IgnoreNicks            string // all protocols | 	EditDisable            bool     // mattermost, slack, discord, telegram, gitter | ||||||
| 	IgnoreMessages         string // all protocols | 	HTMLDisable            bool     // matrix | ||||||
| 	Jid                    string // xmpp | 	IconURL                string   // mattermost, slack | ||||||
| 	Label                  string // all protocols | 	IgnoreFailureOnStart   bool     // general | ||||||
| 	Login                  string // mattermost, matrix | 	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 | 	MediaDownloadBlackList []string | ||||||
| 	MediaDownloadPath      string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server. | 	MediaDownloadPath      string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server. | ||||||
| 	MediaDownloadSize      int    // all protocols | 	MediaDownloadSize      int    // all protocols | ||||||
| 	MediaServerDownload    string | 	MediaServerDownload    string | ||||||
| 	MediaServerUpload      string | 	MediaServerUpload      string | ||||||
|  | 	MediaConvertTgs        string     // telegram | ||||||
| 	MediaConvertWebPToPNG  bool       // telegram | 	MediaConvertWebPToPNG  bool       // telegram | ||||||
| 	MessageDelay           int        // IRC, time in millisecond to wait between messages | 	MessageDelay           int        // IRC, time in millisecond to wait between messages | ||||||
| 	MessageFormat          string     // telegram | 	MessageFormat          string     // telegram | ||||||
| @@ -100,6 +122,7 @@ type Protocol struct { | |||||||
| 	MessageQueue           int        // IRC, size of message queue for flood control | 	MessageQueue           int        // IRC, size of message queue for flood control | ||||||
| 	MessageSplit           bool       // IRC, split long messages with newlines on MessageLength instead of clipping | 	MessageSplit           bool       // IRC, split long messages with newlines on MessageLength instead of clipping | ||||||
| 	Muc                    string     // xmpp | 	Muc                    string     // xmpp | ||||||
|  | 	MxID                   string     // matrix | ||||||
| 	Name                   string     // all protocols | 	Name                   string     // all protocols | ||||||
| 	Nick                   string     // all protocols | 	Nick                   string     // all protocols | ||||||
| 	NickFormatter          string     // mattermost, slack | 	NickFormatter          string     // mattermost, slack | ||||||
| @@ -109,19 +132,22 @@ type Protocol struct { | |||||||
| 	NicksPerRow            int        // mattermost, slack | 	NicksPerRow            int        // mattermost, slack | ||||||
| 	NoHomeServerSuffix     bool       // matrix | 	NoHomeServerSuffix     bool       // matrix | ||||||
| 	NoSendJoinPart         bool       // all protocols | 	NoSendJoinPart         bool       // all protocols | ||||||
| 	NoTLS                  bool       // mattermost | 	NoTLS                  bool       // mattermost, xmpp | ||||||
| 	Password               string     // IRC,mattermost,XMPP,matrix | 	Password               string     // IRC,mattermost,XMPP,matrix | ||||||
| 	PrefixMessagesWithNick bool       // mattemost, slack | 	PrefixMessagesWithNick bool       // mattemost, slack | ||||||
| 	PreserveThreading      bool       // slack | 	PreserveThreading      bool       // slack | ||||||
| 	Protocol               string     // all protocols | 	Protocol               string     // all protocols | ||||||
| 	QuoteDisable           bool       // telegram | 	QuoteDisable           bool       // telegram | ||||||
| 	QuoteFormat            string     // telegram | 	QuoteFormat            string     // telegram | ||||||
|  | 	QuoteLengthLimit       int        // telegram | ||||||
|  | 	RealName               string     // IRC | ||||||
| 	RejoinDelay            int        // IRC | 	RejoinDelay            int        // IRC | ||||||
| 	ReplaceMessages        [][]string // all protocols | 	ReplaceMessages        [][]string // all protocols | ||||||
| 	ReplaceNicks           [][]string // all protocols | 	ReplaceNicks           [][]string // all protocols | ||||||
| 	RemoteNickFormat       string     // all protocols | 	RemoteNickFormat       string     // all protocols | ||||||
| 	RunCommands            []string   // IRC | 	RunCommands            []string   // IRC | ||||||
| 	Server                 string     // IRC,mattermost,XMPP,discord | 	Server                 string     // IRC,mattermost,XMPP,discord,matrix | ||||||
|  | 	SessionFile            string     // msteams,whatsapp | ||||||
| 	ShowJoinPart           bool       // all protocols | 	ShowJoinPart           bool       // all protocols | ||||||
| 	ShowTopicChange        bool       // slack | 	ShowTopicChange        bool       // slack | ||||||
| 	ShowUserTyping         bool       // slack | 	ShowUserTyping         bool       // slack | ||||||
| @@ -129,19 +155,24 @@ type Protocol struct { | |||||||
| 	SkipTLSVerify          bool       // IRC, mattermost | 	SkipTLSVerify          bool       // IRC, mattermost | ||||||
| 	SkipVersionCheck       bool       // mattermost | 	SkipVersionCheck       bool       // mattermost | ||||||
| 	StripNick              bool       // all protocols | 	StripNick              bool       // all protocols | ||||||
|  | 	StripMarkdown          bool       // irc | ||||||
| 	SyncTopic              bool       // slack | 	SyncTopic              bool       // slack | ||||||
| 	TengoModifyMessage     string     // general | 	TengoModifyMessage     string     // general | ||||||
| 	Team                   string     // mattermost, keybase | 	Team                   string     // mattermost, keybase | ||||||
| 	Token                  string     // gitter, slack, discord, api | 	TeamID                 string     // msteams | ||||||
|  | 	TenantID               string     // msteams | ||||||
|  | 	Token                  string     // gitter, slack, discord, api, matrix | ||||||
| 	Topic                  string     // zulip | 	Topic                  string     // zulip | ||||||
| 	URL                    string     // mattermost, slack // DEPRECATED | 	URL                    string     // mattermost, slack // DEPRECATED | ||||||
| 	UseAPI                 bool       // mattermost, slack | 	UseAPI                 bool       // mattermost, slack | ||||||
|  | 	UseLocalAvatar         []string   // discord | ||||||
| 	UseSASL                bool       // IRC | 	UseSASL                bool       // IRC | ||||||
| 	UseTLS                 bool       // IRC | 	UseTLS                 bool       // IRC | ||||||
| 	UseDiscriminator       bool       // discord | 	UseDiscriminator       bool       // discord | ||||||
| 	UseFirstName           bool       // telegram | 	UseFirstName           bool       // telegram | ||||||
| 	UseUserName            bool       // discord | 	UseUserName            bool       // discord, matrix, mattermost | ||||||
| 	UseInsecureURL         bool       // telegram | 	UseInsecureURL         bool       // telegram | ||||||
|  | 	UserName               string     // IRC | ||||||
| 	VerboseJoinPart        bool       // IRC | 	VerboseJoinPart        bool       // IRC | ||||||
| 	WebhookBindAddress     string     // mattermost, slack | 	WebhookBindAddress     string     // mattermost, slack | ||||||
| 	WebhookURL             string     // mattermost, slack | 	WebhookURL             string     // mattermost, slack | ||||||
| @@ -199,6 +230,7 @@ type BridgeValues struct { | |||||||
| 	WhatsApp           map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results | 	WhatsApp           map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results | ||||||
| 	Zulip              map[string]Protocol | 	Zulip              map[string]Protocol | ||||||
| 	Keybase            map[string]Protocol | 	Keybase            map[string]Protocol | ||||||
|  | 	Mumble             map[string]Protocol | ||||||
| 	General            Protocol | 	General            Protocol | ||||||
| 	Tengo              Tengo | 	Tengo              Tengo | ||||||
| 	Gateway            []Gateway | 	Gateway            []Gateway | ||||||
| @@ -208,6 +240,7 @@ type BridgeValues struct { | |||||||
| type Config interface { | type Config interface { | ||||||
| 	Viper() *viper.Viper | 	Viper() *viper.Viper | ||||||
| 	BridgeValues() *BridgeValues | 	BridgeValues() *BridgeValues | ||||||
|  | 	IsKeySet(key string) bool | ||||||
| 	GetBool(key string) (bool, bool) | 	GetBool(key string) (bool, bool) | ||||||
| 	GetInt(key string) (int, bool) | 	GetInt(key string) (int, bool) | ||||||
| 	GetString(key string) (string, bool) | 	GetString(key string) (string, bool) | ||||||
| @@ -233,7 +266,17 @@ func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config { | |||||||
| 		logger.Fatalf("Failed to read configuration file: %#v", err) | 		logger.Fatalf("Failed to read configuration file: %#v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	mycfg := newConfigFromString(logger, input) | 	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 { | 	if mycfg.cv.General.MediaDownloadSize == 0 { | ||||||
| 		mycfg.cv.General.MediaDownloadSize = 1000000 | 		mycfg.cv.General.MediaDownloadSize = 1000000 | ||||||
| 	} | 	} | ||||||
| @@ -244,14 +287,26 @@ func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config { | |||||||
| 	return mycfg | 	return mycfg | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // detectConfigType detects JSON and YAML formats, defaults to TOML. | ||||||
|  | func detectConfigType(cfgfile string) string { | ||||||
|  | 	fileExt := filepath.Ext(cfgfile) | ||||||
|  | 	switch fileExt { | ||||||
|  | 	case ".json": | ||||||
|  | 		return "json" | ||||||
|  | 	case ".yaml", ".yml": | ||||||
|  | 		return "yaml" | ||||||
|  | 	} | ||||||
|  | 	return "toml" | ||||||
|  | } | ||||||
|  |  | ||||||
| // NewConfigFromString instantiates a new configuration based on the specified string. | // NewConfigFromString instantiates a new configuration based on the specified string. | ||||||
| func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config { | func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config { | ||||||
| 	logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"}) | 	logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"}) | ||||||
| 	return newConfigFromString(logger, input) | 	return newConfigFromString(logger, input, "toml") | ||||||
| } | } | ||||||
|  |  | ||||||
| func newConfigFromString(logger *logrus.Entry, input []byte) *config { | func newConfigFromString(logger *logrus.Entry, input []byte, cfgtype string) *config { | ||||||
| 	viper.SetConfigType("toml") | 	viper.SetConfigType(cfgtype) | ||||||
| 	viper.SetEnvPrefix("matterbridge") | 	viper.SetEnvPrefix("matterbridge") | ||||||
| 	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) | 	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) | ||||||
| 	viper.AutomaticEnv() | 	viper.AutomaticEnv() | ||||||
| @@ -279,6 +334,12 @@ func (c *config) Viper() *viper.Viper { | |||||||
| 	return c.v | 	return c.v | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (c *config) IsKeySet(key string) bool { | ||||||
|  | 	c.RLock() | ||||||
|  | 	defer c.RUnlock() | ||||||
|  | 	return c.v.IsSet(key) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (c *config) GetBool(key string) (bool, bool) { | func (c *config) GetBool(key string) (bool, bool) { | ||||||
| 	c.RLock() | 	c.RLock() | ||||||
| 	defer c.RUnlock() | 	defer c.RUnlock() | ||||||
| @@ -338,6 +399,11 @@ type TestConfig struct { | |||||||
| 	Overrides map[string]interface{} | 	Overrides map[string]interface{} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (c *TestConfig) IsKeySet(key string) bool { | ||||||
|  | 	_, ok := c.Overrides[key] | ||||||
|  | 	return ok || c.Config.IsKeySet(key) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (c *TestConfig) GetBool(key string) (bool, bool) { | func (c *TestConfig) GetBool(key string) (bool, bool) { | ||||||
| 	val, ok := c.Overrides[key] | 	val, ok := c.Overrides[key] | ||||||
| 	if ok { | 	if ok { | ||||||
|   | |||||||
| @@ -2,30 +2,31 @@ package bdiscord | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/discord/transmitter" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/bwmarrin/discordgo" | 	lru "github.com/hashicorp/golang-lru" | ||||||
|  | 	"github.com/matterbridge/discordgo" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const MessageLength = 1950 | const ( | ||||||
|  | 	MessageLength = 1950 | ||||||
|  | 	cFileUpload   = "file_upload" | ||||||
|  | ) | ||||||
|  |  | ||||||
| type Bdiscord struct { | type Bdiscord struct { | ||||||
| 	*bridge.Config | 	*bridge.Config | ||||||
|  |  | ||||||
| 	c *discordgo.Session | 	c *discordgo.Session | ||||||
|  |  | ||||||
| 	nick            string | 	nick    string | ||||||
| 	useChannelID    bool | 	userID  string | ||||||
| 	guildID         string | 	guildID string | ||||||
| 	webhookID       string |  | ||||||
| 	webhookToken    string |  | ||||||
| 	canEditWebhooks bool |  | ||||||
|  |  | ||||||
| 	channelsMutex  sync.RWMutex | 	channelsMutex  sync.RWMutex | ||||||
| 	channels       []*discordgo.Channel | 	channels       []*discordgo.Channel | ||||||
| @@ -34,30 +35,39 @@ type Bdiscord struct { | |||||||
| 	membersMutex  sync.RWMutex | 	membersMutex  sync.RWMutex | ||||||
| 	userMemberMap map[string]*discordgo.Member | 	userMemberMap map[string]*discordgo.Member | ||||||
| 	nickMemberMap map[string]*discordgo.Member | 	nickMemberMap map[string]*discordgo.Member | ||||||
|  |  | ||||||
|  | 	// Webhook specific logic | ||||||
|  | 	useAutoWebhooks bool | ||||||
|  | 	transmitter     *transmitter.Transmitter | ||||||
|  | 	cache           *lru.Cache | ||||||
| } | } | ||||||
|  |  | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { | 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.userMemberMap = make(map[string]*discordgo.Member) | ||||||
| 	b.nickMemberMap = make(map[string]*discordgo.Member) | 	b.nickMemberMap = make(map[string]*discordgo.Member) | ||||||
| 	b.channelInfoMap = make(map[string]*config.ChannelInfo) | 	b.channelInfoMap = make(map[string]*config.ChannelInfo) | ||||||
| 	if b.GetString("WebhookURL") != "" { |  | ||||||
| 		b.Log.Debug("Configuring Discord Incoming Webhook") | 	b.useAutoWebhooks = b.GetBool("AutoWebhooks") | ||||||
| 		b.webhookID, b.webhookToken = b.splitURL(b.GetString("WebhookURL")) | 	if b.useAutoWebhooks { | ||||||
|  | 		b.Log.Debug("Using automatic webhooks") | ||||||
| 	} | 	} | ||||||
| 	return b | 	return b | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bdiscord) Connect() error { | func (b *Bdiscord) Connect() error { | ||||||
| 	var err error | 	var err error | ||||||
| 	var guildFound bool |  | ||||||
| 	token := b.GetString("Token") | 	token := b.GetString("Token") | ||||||
| 	b.Log.Info("Connecting") | 	b.Log.Info("Connecting") | ||||||
| 	if b.GetString("WebhookURL") == "" { |  | ||||||
| 		b.Log.Info("Connecting using token") |  | ||||||
| 	} else { |  | ||||||
| 		b.Log.Info("Connecting using webhookurl (for posting) and token") |  | ||||||
| 	} |  | ||||||
| 	if !strings.HasPrefix(b.GetString("Token"), "Bot ") { | 	if !strings.HasPrefix(b.GetString("Token"), "Bot ") { | ||||||
| 		token = "Bot " + b.GetString("Token") | 		token = "Bot " + b.GetString("Token") | ||||||
| 	} | 	} | ||||||
| @@ -79,6 +89,14 @@ func (b *Bdiscord) Connect() error { | |||||||
| 	b.c.AddHandler(b.messageDeleteBulk) | 	b.c.AddHandler(b.messageDeleteBulk) | ||||||
| 	b.c.AddHandler(b.memberAdd) | 	b.c.AddHandler(b.memberAdd) | ||||||
| 	b.c.AddHandler(b.memberRemove) | 	b.c.AddHandler(b.memberRemove) | ||||||
|  | 	if b.GetInt("debuglevel") == 1 { | ||||||
|  | 		b.c.AddHandler(b.messageEvent) | ||||||
|  | 	} | ||||||
|  | 	// Add privileged intent for guild member tracking. This is needed to track nicks | ||||||
|  | 	// for display names and @mention translation | ||||||
|  | 	b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged | | ||||||
|  | 		discordgo.IntentsGuildMembers) | ||||||
|  |  | ||||||
| 	err = b.c.Open() | 	err = b.c.Open() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -93,55 +111,108 @@ func (b *Bdiscord) Connect() error { | |||||||
| 	} | 	} | ||||||
| 	serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1) | 	serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1) | ||||||
| 	b.nick = userinfo.Username | 	b.nick = userinfo.Username | ||||||
|  | 	b.userID = userinfo.ID | ||||||
|  |  | ||||||
|  | 	// Try and find this account's guild, and populate channels | ||||||
| 	b.channelsMutex.Lock() | 	b.channelsMutex.Lock() | ||||||
| 	for _, guild := range guilds { | 	for _, guild := range guilds { | ||||||
| 		if guild.Name == serverName || guild.ID == serverName { | 		// Skip, if the server name does not match the visible name or the ID | ||||||
| 			b.channels, err = b.c.GuildChannels(guild.ID) | 		if guild.Name != serverName && guild.ID != serverName { | ||||||
| 			if err != nil { | 			continue | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 			b.guildID = guild.ID |  | ||||||
| 			guildFound = true |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// Complain about an ambiguous Server setting. Two Discord servers could have the same title! | ||||||
|  | 		// For IDs, practically this will never happen. It would only trigger if some server's name is also an ID. | ||||||
|  | 		if b.guildID != "" { | ||||||
|  | 			return fmt.Errorf("found multiple Discord servers with the same name %#v, expected to see only one", serverName) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Getting this guild's channel could result in a permission error | ||||||
|  | 		b.channels, err = b.c.GuildChannels(guild.ID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("could not get %#v's channels: %w", b.GetString("Server"), err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		b.guildID = guild.ID | ||||||
| 	} | 	} | ||||||
| 	b.channelsMutex.Unlock() | 	b.channelsMutex.Unlock() | ||||||
| 	if !guildFound { |  | ||||||
| 		msg := fmt.Sprintf("Server \"%s\" not found", b.GetString("Server")) |  | ||||||
| 		err = errors.New(msg) |  | ||||||
| 		b.Log.Error(msg) |  | ||||||
| 		b.Log.Info("Possible values:") |  | ||||||
| 		for _, guild := range guilds { |  | ||||||
| 			b.Log.Infof("Server=\"%s\" # Server name", guild.Name) |  | ||||||
| 			b.Log.Infof("Server=\"%s\" # Server ID", guild.ID) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err != nil { | 	// 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("\t- Server=%#v # by name", guild.Name) | ||||||
|  | 			b.Log.Infof("\t- Server=%#v # by ID", guild.ID) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// If there are no results, we should say that | ||||||
|  | 		if len(guilds) == 0 { | ||||||
|  | 			b.Log.Info("\t- (none found)") | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	b.channelsMutex.RLock() |  | ||||||
| 	if b.GetString("WebhookURL") == "" { | 	// Legacy note: WebhookURL used to have an actual webhook URL that we would edit, | ||||||
| 		for _, channel := range b.channels { | 	// but we stopped doing that due to Discord making rate limits more aggressive. | ||||||
| 			b.Log.Debugf("found channel %#v", channel) | 	// | ||||||
| 		} | 	// Even older: the same WebhookURL used to be used by every channel, which is usually unexpected. | ||||||
| 	} else { | 	// This is no longer possible. | ||||||
| 		b.canEditWebhooks = true | 	if b.GetString("WebhookURL") != "" { | ||||||
| 		for _, channel := range b.channels { | 		message := "The global WebhookURL setting has been removed. " | ||||||
| 			b.Log.Debugf("found channel %#v; verifying PermissionManageWebhooks", channel) | 		message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. " | ||||||
| 			perms, permsErr := b.c.State.UserChannelPermissions(userinfo.ID, channel.ID) | 		message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections." | ||||||
| 			manageWebhooks := discordgo.PermissionManageWebhooks | 		b.Log.Errorln(message) | ||||||
| 			if permsErr != nil || perms&manageWebhooks != manageWebhooks { | 		return fmt.Errorf("use of removed WebhookURL setting") | ||||||
| 				b.Log.Warnf("Can't manage webhooks in channel \"%s\"", channel.Name) | 	} | ||||||
| 				b.canEditWebhooks = false |  | ||||||
|  | 	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) | ||||||
| 			} | 			} | ||||||
|  | 			continue | ||||||
| 		} | 		} | ||||||
| 		if b.canEditWebhooks { |  | ||||||
| 			b.Log.Info("Can manage webhooks; will edit channel for global webhook on send") | 		whID, whToken, ok := b.splitURL(channel.Options.WebhookURL) | ||||||
| 		} else { | 		if !ok { | ||||||
| 			b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send") | 			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. | 	// Obtaining guild members and initializing nickname mapping. | ||||||
| 	b.membersMutex.Lock() | 	b.membersMutex.Lock() | ||||||
| @@ -174,10 +245,6 @@ func (b *Bdiscord) JoinChannel(channel config.ChannelInfo) error { | |||||||
| 	defer b.channelsMutex.Unlock() | 	defer b.channelsMutex.Unlock() | ||||||
|  |  | ||||||
| 	b.channelInfoMap[channel.ID] = &channel | 	b.channelInfoMap[channel.ID] = &channel | ||||||
| 	idcheck := strings.Split(channel.Name, "ID:") |  | ||||||
| 	if len(idcheck) > 1 { |  | ||||||
| 		b.useChannelID = true |  | ||||||
| 	} |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -202,76 +269,23 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { | |||||||
| 		msg.Text = "_" + msg.Text + "_" | 		msg.Text = "_" + msg.Text + "_" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// use initial webhook configured for the entire Discord account | 	// Handle prefix hint for unthreaded messages. | ||||||
| 	isGlobalWebhook := true | 	if msg.ParentNotFound() { | ||||||
| 	wID := b.webhookID | 		msg.ParentID = "" | ||||||
| 	wToken := b.webhookToken | 		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) | ||||||
|  |  | ||||||
| 	// check if have a channel specific webhook |  | ||||||
| 	b.channelsMutex.RLock() |  | ||||||
| 	if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok { |  | ||||||
| 		if ci.Options.WebhookURL != "" { |  | ||||||
| 			wID, wToken = b.splitURL(ci.Options.WebhookURL) |  | ||||||
| 			isGlobalWebhook = false |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 	b.channelsMutex.RUnlock() |  | ||||||
|  |  | ||||||
| 	// Use webhook to send the message | 	// Use webhook to send the message | ||||||
| 	if wID != "" && msg.Event != config.EventMsgDelete { | 	useWebhooks := b.shouldMessageUseWebhooks(&msg) | ||||||
| 		// skip events | 	if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" { | ||||||
| 		if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange { | 		return b.handleEventWebhook(&msg, channelID) | ||||||
| 			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 |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	return b.handleEventBotUser(&msg, channelID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleEventDirect handles events via the bot user | ||||||
|  | func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) { | ||||||
| 	b.Log.Debugf("Broadcasting using token (API)") | 	b.Log.Debugf("Broadcasting using token (API)") | ||||||
|  |  | ||||||
| 	// Delete message | 	// Delete message | ||||||
| @@ -283,21 +297,36 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { | |||||||
| 		return "", err | 		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 | 	// Upload a file if it exists | ||||||
| 	if msg.Extra != nil { | 	if msg.Extra != nil { | ||||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | 		for _, rmsg := range helper.HandleExtra(msg, b.General) { | ||||||
| 			rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength) | 			rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped")) | ||||||
| 			if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil { | 			if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil { | ||||||
| 				b.Log.Errorf("Could not send message %#v: %s", rmsg, err) | 				b.Log.Errorf("Could not send message %#v: %s", rmsg, err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		// check if we have files to upload (from slack, telegram or mattermost) | 		// check if we have files to upload (from slack, telegram or mattermost) | ||||||
| 		if len(msg.Extra["file"]) > 0 { | 		if len(msg.Extra["file"]) > 0 { | ||||||
| 			return b.handleUploadFile(&msg, channelID) | 			return b.handleUploadFile(msg, channelID) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	msg.Text = helper.ClipMessage(msg.Text, MessageLength) | 	msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped")) | ||||||
| 	msg.Text = b.replaceUserMentions(msg.Text) | 	msg.Text = b.replaceUserMentions(msg.Text) | ||||||
|  |  | ||||||
| 	// Edit message | 	// Edit message | ||||||
| @@ -306,57 +335,30 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { | |||||||
| 		return msg.ID, err | 		return msg.ID, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	m := discordgo.MessageSend{ | ||||||
|  | 		Content:         msg.Username + msg.Text, | ||||||
|  | 		AllowedMentions: b.getAllowedMentions(), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if msg.ParentValid() { | ||||||
|  | 		m.Reference = &discordgo.MessageReference{ | ||||||
|  | 			MessageID: msg.ParentID, | ||||||
|  | 			ChannelID: channelID, | ||||||
|  | 			GuildID:   b.guildID, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Post normal message | 	// Post normal message | ||||||
| 	res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text) | 	res, err := b.c.ChannelMessageSendComplex(channelID, &m) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return res.ID, nil | 	return res.ID, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // useWebhook returns true if we have a webhook defined somewhere |  | ||||||
| func (b *Bdiscord) useWebhook() bool { |  | ||||||
| 	if b.GetString("WebhookURL") != "" { |  | ||||||
| 		return true |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	b.channelsMutex.RLock() |  | ||||||
| 	defer b.channelsMutex.RUnlock() |  | ||||||
|  |  | ||||||
| 	for _, channel := range b.channelInfoMap { |  | ||||||
| 		if channel.Options.WebhookURL != "" { |  | ||||||
| 			return true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return false |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // isWebhookID returns true if the specified id is used in a defined webhook |  | ||||||
| func (b *Bdiscord) isWebhookID(id string) bool { |  | ||||||
| 	if b.GetString("WebhookURL") != "" { |  | ||||||
| 		wID, _ := b.splitURL(b.GetString("WebhookURL")) |  | ||||||
| 		if wID == id { |  | ||||||
| 			return true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	b.channelsMutex.RLock() |  | ||||||
| 	defer b.channelsMutex.RUnlock() |  | ||||||
|  |  | ||||||
| 	for _, channel := range b.channelInfoMap { |  | ||||||
| 		if channel.Options.WebhookURL != "" { |  | ||||||
| 			wID, _ := b.splitURL(channel.Options.WebhookURL) |  | ||||||
| 			if wID == id { |  | ||||||
| 				return true |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return false |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // handleUploadFile handles native upload of files | // handleUploadFile handles native upload of files | ||||||
| func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) { | func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) { | ||||||
| 	var err error |  | ||||||
| 	for _, f := range msg.Extra["file"] { | 	for _, f := range msg.Extra["file"] { | ||||||
| 		fi := f.(config.FileInfo) | 		fi := f.(config.FileInfo) | ||||||
| 		file := discordgo.File{ | 		file := discordgo.File{ | ||||||
| @@ -365,67 +367,19 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri | |||||||
| 			Reader:      bytes.NewReader(*fi.Data), | 			Reader:      bytes.NewReader(*fi.Data), | ||||||
| 		} | 		} | ||||||
| 		m := discordgo.MessageSend{ | 		m := discordgo.MessageSend{ | ||||||
| 			Content: msg.Username + fi.Comment, | 			Content:         msg.Username + fi.Comment, | ||||||
| 			Files:   []*discordgo.File{&file}, | 			Files:           []*discordgo.File{&file}, | ||||||
|  | 			AllowedMentions: b.getAllowedMentions(), | ||||||
| 		} | 		} | ||||||
| 		_, err = b.c.ChannelMessageSendComplex(channelID, &m) | 		res, err := b.c.ChannelMessageSendComplex(channelID, &m) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return "", fmt.Errorf("file upload failed: %s", err) | 			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 | 	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 |  | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	// 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), |  | ||||||
| 			} |  | ||||||
| 			_, e2 := b.c.WebhookExecute( |  | ||||||
| 				webhookID, |  | ||||||
| 				token, |  | ||||||
| 				false, |  | ||||||
| 				&discordgo.WebhookParams{ |  | ||||||
| 					Username:  msg.Username, |  | ||||||
| 					AvatarURL: msg.Avatar, |  | ||||||
| 					File:      &file, |  | ||||||
| 				}, |  | ||||||
| 			) |  | ||||||
| 			if e2 != nil { |  | ||||||
| 				b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return res, err |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -2,15 +2,14 @@ package bdiscord | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/bwmarrin/discordgo" | 	"github.com/davecgh/go-spew/spew" | ||||||
|  | 	"github.com/matterbridge/discordgo" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam | func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam | ||||||
| 	rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete} | 	rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete} | ||||||
| 	rmsg.Channel = b.getChannelName(m.ChannelID) | 	rmsg.Channel = b.getChannelName(m.ChannelID) | ||||||
| 	if b.useChannelID { |  | ||||||
| 		rmsg.Channel = "ID:" + m.ChannelID |  | ||||||
| 	} |  | ||||||
| 	b.Log.Debugf("<= Sending message from %s to gateway", b.Account) | 	b.Log.Debugf("<= Sending message from %s to gateway", b.Account) | ||||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
| 	b.Remote <- rmsg | 	b.Remote <- rmsg | ||||||
| @@ -24,11 +23,7 @@ func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageD | |||||||
| 			ID:      msgID, | 			ID:      msgID, | ||||||
| 			Event:   config.EventMsgDelete, | 			Event:   config.EventMsgDelete, | ||||||
| 			Text:    config.EventMsgDelete, | 			Text:    config.EventMsgDelete, | ||||||
| 			Channel: "ID:" + m.ChannelID, | 			Channel: b.getChannelName(m.ChannelID), | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if !b.useChannelID { |  | ||||||
| 			rmsg.Channel = b.getChannelName(m.ChannelID) |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		b.Log.Debugf("<= Sending message from %s to gateway", b.Account) | 		b.Log.Debugf("<= Sending message from %s to gateway", b.Account) | ||||||
| @@ -37,16 +32,22 @@ 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) { | func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) { | ||||||
| 	if !b.GetBool("ShowUserTyping") { | 	if !b.GetBool("ShowUserTyping") { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Ignore our own typing messages | ||||||
|  | 	if m.UserID == b.userID { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	rmsg := config.Message{Account: b.Account, Event: config.EventUserTyping} | 	rmsg := config.Message{Account: b.Account, Event: config.EventUserTyping} | ||||||
| 	rmsg.Channel = b.getChannelName(m.ChannelID) | 	rmsg.Channel = b.getChannelName(m.ChannelID) | ||||||
| 	if b.useChannelID { |  | ||||||
| 		rmsg.Channel = "ID:" + m.ChannelID |  | ||||||
| 	} |  | ||||||
| 	b.Remote <- rmsg | 	b.Remote <- rmsg | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -73,7 +74,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	// if using webhooks, do not relay if it's ours | 	// if using webhooks, do not relay if it's ours | ||||||
| 	if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) { | 	if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -86,9 +87,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} | 	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 != "" { | 	if m.Content != "" { | ||||||
| 		b.Log.Debugf("== Receiving event %#v", m.Message) |  | ||||||
| 		m.Message.Content = b.stripCustomoji(m.Message.Content) |  | ||||||
| 		m.Message.Content = b.replaceChannelMentions(m.Message.Content) | 		m.Message.Content = b.replaceChannelMentions(m.Message.Content) | ||||||
| 		rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c) | 		rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -99,16 +100,13 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat | |||||||
|  |  | ||||||
| 	// set channel name | 	// set channel name | ||||||
| 	rmsg.Channel = b.getChannelName(m.ChannelID) | 	rmsg.Channel = b.getChannelName(m.ChannelID) | ||||||
| 	if b.useChannelID { |  | ||||||
| 		rmsg.Channel = "ID:" + m.ChannelID |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// set username | 	fromWebhook := m.WebhookID != "" | ||||||
| 	if !b.GetBool("UseUserName") { | 	if !fromWebhook && !b.GetBool("UseUserName") { | ||||||
| 		rmsg.Username = b.getNick(m.Author, m.GuildID) | 		rmsg.Username = b.getNick(m.Author, m.GuildID) | ||||||
| 	} else { | 	} else { | ||||||
| 		rmsg.Username = m.Author.Username | 		rmsg.Username = m.Author.Username | ||||||
| 		if b.GetBool("UseDiscriminator") { | 		if !fromWebhook && b.GetBool("UseDiscriminator") { | ||||||
| 			rmsg.Username += "#" + m.Author.Discriminator | 			rmsg.Username += "#" + m.Author.Discriminator | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -116,7 +114,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat | |||||||
| 	// if we have embedded content add it to text | 	// if we have embedded content add it to text | ||||||
| 	if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil { | 	if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil { | ||||||
| 		for _, embed := range m.Message.Embeds { | 		for _, embed := range m.Message.Embeds { | ||||||
| 			rmsg.Text = rmsg.Text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n" | 			rmsg.Text += handleEmbed(embed) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -132,6 +130,14 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat | |||||||
| 		rmsg.Event = config.EventUserAction | 		rmsg.Event = config.EventUserAction | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Replace emotes | ||||||
|  | 	rmsg.Text = replaceEmotes(rmsg.Text) | ||||||
|  |  | ||||||
|  | 	// Add our parent id if it exists, and if it's not referring to a message in another channel | ||||||
|  | 	if ref := m.MessageReference; ref != nil && ref.ChannelID == m.ChannelID { | ||||||
|  | 		rmsg.ParentID = ref.MessageID | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account) | 	b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account) | ||||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
| 	b.Remote <- rmsg | 	b.Remote <- rmsg | ||||||
| @@ -205,3 +211,33 @@ func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRe | |||||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
| 	b.Remote <- rmsg | 	b.Remote <- rmsg | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func handleEmbed(embed *discordgo.MessageEmbed) string { | ||||||
|  | 	var t []string | ||||||
|  | 	var result string | ||||||
|  |  | ||||||
|  | 	t = append(t, embed.Title) | ||||||
|  | 	t = append(t, embed.Description) | ||||||
|  | 	t = append(t, embed.URL) | ||||||
|  |  | ||||||
|  | 	i := 0 | ||||||
|  | 	for _, e := range t { | ||||||
|  | 		if e == "" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		i++ | ||||||
|  | 		if i == 1 { | ||||||
|  | 			result += " embed: " + e | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		result += " - " + e | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if result != "" { | ||||||
|  | 		result += "\n" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										58
									
								
								bridge/discord/handlers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								bridge/discord/handlers_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | package bdiscord | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/matterbridge/discordgo" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestHandleEmbed(t *testing.T) { | ||||||
|  | 	testcases := map[string]struct { | ||||||
|  | 		embed  *discordgo.MessageEmbed | ||||||
|  | 		result string | ||||||
|  | 	}{ | ||||||
|  | 		"allempty": { | ||||||
|  | 			embed:  &discordgo.MessageEmbed{}, | ||||||
|  | 			result: "", | ||||||
|  | 		}, | ||||||
|  | 		"one": { | ||||||
|  | 			embed: &discordgo.MessageEmbed{ | ||||||
|  | 				Title: "blah", | ||||||
|  | 			}, | ||||||
|  | 			result: " embed: blah\n", | ||||||
|  | 		}, | ||||||
|  | 		"two": { | ||||||
|  | 			embed: &discordgo.MessageEmbed{ | ||||||
|  | 				Title:       "blah", | ||||||
|  | 				Description: "blah2", | ||||||
|  | 			}, | ||||||
|  | 			result: " embed: blah - blah2\n", | ||||||
|  | 		}, | ||||||
|  | 		"three": { | ||||||
|  | 			embed: &discordgo.MessageEmbed{ | ||||||
|  | 				Title:       "blah", | ||||||
|  | 				Description: "blah2", | ||||||
|  | 				URL:         "blah3", | ||||||
|  | 			}, | ||||||
|  | 			result: " embed: blah - blah2 - blah3\n", | ||||||
|  | 		}, | ||||||
|  | 		"twob": { | ||||||
|  | 			embed: &discordgo.MessageEmbed{ | ||||||
|  | 				Description: "blah2", | ||||||
|  | 				URL:         "blah3", | ||||||
|  | 			}, | ||||||
|  | 			result: " embed: blah2 - blah3\n", | ||||||
|  | 		}, | ||||||
|  | 		"oneb": { | ||||||
|  | 			embed: &discordgo.MessageEmbed{ | ||||||
|  | 				URL: "blah3", | ||||||
|  | 			}, | ||||||
|  | 			result: " embed: blah3\n", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for name, tc := range testcases { | ||||||
|  | 		assert.Equalf(t, tc.result, handleEmbed(tc.embed), "Testcases %s", name) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -6,9 +6,33 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"unicode" | 	"unicode" | ||||||
|  |  | ||||||
| 	"github.com/bwmarrin/discordgo" | 	"github.com/matterbridge/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 { | func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string { | ||||||
| 	b.membersMutex.RLock() | 	b.membersMutex.RLock() | ||||||
| 	defer b.membersMutex.RUnlock() | 	defer b.membersMutex.RUnlock() | ||||||
| @@ -96,6 +120,13 @@ func (b *Bdiscord) getChannelName(id string) string { | |||||||
| 	b.channelsMutex.RLock() | 	b.channelsMutex.RLock() | ||||||
| 	defer b.channelsMutex.RUnlock() | 	defer b.channelsMutex.RUnlock() | ||||||
|  |  | ||||||
|  | 	for _, c := range b.channelInfoMap { | ||||||
|  | 		if c.Name == "ID:"+id { | ||||||
|  | 			// if we have ID: specified in our gateway configuration return this | ||||||
|  | 			return c.Name | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	for _, channel := range b.channels { | 	for _, channel := range b.channels { | ||||||
| 		if channel.ID == id { | 		if channel.ID == id { | ||||||
| 			return b.getCategoryChannelName(channel.Name, channel.ParentID) | 			return b.getCategoryChannelName(channel.Name, channel.ParentID) | ||||||
| @@ -129,8 +160,8 @@ func (b *Bdiscord) getCategoryChannelName(name, parentID string) string { | |||||||
| var ( | var ( | ||||||
| 	// See https://discordapp.com/developers/docs/reference#message-formatting. | 	// See https://discordapp.com/developers/docs/reference#message-formatting. | ||||||
| 	channelMentionRE = regexp.MustCompile("<#[0-9]+>") | 	channelMentionRE = regexp.MustCompile("<#[0-9]+>") | ||||||
| 	emojiRE          = regexp.MustCompile("<(:.*?:)[0-9]+>") |  | ||||||
| 	userMentionRE    = regexp.MustCompile("@[^@\n]{1,32}") | 	userMentionRE    = regexp.MustCompile("@[^@\n]{1,32}") | ||||||
|  | 	emoteRE          = regexp.MustCompile(`<a?(:\w+:)\d+>`) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (b *Bdiscord) replaceChannelMentions(text string) string { | func (b *Bdiscord) replaceChannelMentions(text string) string { | ||||||
| @@ -176,19 +207,20 @@ func (b *Bdiscord) replaceUserMentions(text string) string { | |||||||
| 	return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc) | 	return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bdiscord) stripCustomoji(text string) string { | func replaceEmotes(text string) string { | ||||||
| 	return emojiRE.ReplaceAllString(text, `$1`) | 	return emoteRE.ReplaceAllString(text, "$1") | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bdiscord) replaceAction(text string) (string, bool) { | func (b *Bdiscord) replaceAction(text string) (string, bool) { | ||||||
| 	if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") { | 	length := len(text) | ||||||
| 		return text[1 : len(text)-1], true | 	if length > 1 && text[0] == '_' && text[length-1] == '_' { | ||||||
|  | 		return text[1 : length-1], true | ||||||
| 	} | 	} | ||||||
| 	return text, false | 	return text, false | ||||||
| } | } | ||||||
|  |  | ||||||
| // splitURL splits a webhookURL and returns the ID and token. | // splitURL splits a webhookURL and returns the ID and token. | ||||||
| func (b *Bdiscord) splitURL(url string) (string, string) { | func (b *Bdiscord) splitURL(url string) (string, string, bool) { | ||||||
| 	const ( | 	const ( | ||||||
| 		expectedWebhookSplitCount = 7 | 		expectedWebhookSplitCount = 7 | ||||||
| 		webhookIdxID              = 5 | 		webhookIdxID              = 5 | ||||||
| @@ -196,9 +228,9 @@ func (b *Bdiscord) splitURL(url string) (string, string) { | |||||||
| 	) | 	) | ||||||
| 	webhookURLSplit := strings.Split(url, "/") | 	webhookURLSplit := strings.Split(url, "/") | ||||||
| 	if len(webhookURLSplit) != expectedWebhookSplitCount { | 	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 { | func enumerateUsernames(s string) []string { | ||||||
|   | |||||||
							
								
								
									
										257
									
								
								bridge/discord/transmitter/transmitter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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/matterbridge/discordgo" | ||||||
|  | 	log "github.com/sirupsen/logrus" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // A Transmitter represents a message manager for a single guild. | ||||||
|  | type Transmitter struct { | ||||||
|  | 	session    *discordgo.Session | ||||||
|  | 	guild      string | ||||||
|  | 	title      string | ||||||
|  | 	autoCreate bool | ||||||
|  |  | ||||||
|  | 	// channelWebhooks maps from a channel ID to a webhook instance | ||||||
|  | 	channelWebhooks map[string]*discordgo.Webhook | ||||||
|  |  | ||||||
|  | 	mutex sync.RWMutex | ||||||
|  |  | ||||||
|  | 	Log *log.Entry | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ErrWebhookNotFound is returned when a valid webhook for this channel/message combination does not exist | ||||||
|  | var ErrWebhookNotFound = errors.New("webhook for this channel and message does not exist") | ||||||
|  |  | ||||||
|  | // ErrPermissionDenied is returned if the bot does not have permission to manage webhooks. | ||||||
|  | // | ||||||
|  | // Bots can be granted a guild-wide permission and channel-specific permissions to manage webhooks. | ||||||
|  | // Despite potentially having guild-wide permission, channel specific overrides could deny a bot's permission to manage webhooks. | ||||||
|  | var ErrPermissionDenied = errors.New("missing 'Manage Webhooks' permission") | ||||||
|  |  | ||||||
|  | // New returns a new Transmitter given a Discord session, guild ID, and title. | ||||||
|  | func New(session *discordgo.Session, guild string, title string, autoCreate bool) *Transmitter { | ||||||
|  | 	return &Transmitter{ | ||||||
|  | 		session:    session, | ||||||
|  | 		guild:      guild, | ||||||
|  | 		title:      title, | ||||||
|  | 		autoCreate: autoCreate, | ||||||
|  |  | ||||||
|  | 		channelWebhooks: make(map[string]*discordgo.Webhook), | ||||||
|  |  | ||||||
|  | 		Log: log.NewEntry(log.StandardLogger()), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Send transmits a message to the given channel with the provided webhook data, and waits until Discord responds with message data. | ||||||
|  | func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) { | ||||||
|  | 	wh, err := t.getOrCreateWebhook(channelID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	msg, err := t.session.WebhookExecute(wh.ID, wh.Token, true, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("execute failed: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return msg, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Edit will edit a message in a channel, if possible. | ||||||
|  | func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo.WebhookParams) error { | ||||||
|  | 	wh := t.getWebhook(channelID) | ||||||
|  |  | ||||||
|  | 	if wh == nil { | ||||||
|  | 		return ErrWebhookNotFound | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	uri := discordgo.EndpointWebhookToken(wh.ID, wh.Token) + "/messages/" + messageID | ||||||
|  | 	_, err := t.session.RequestWithBucketID("PATCH", uri, params, discordgo.EndpointWebhookToken("", "")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HasWebhook checks whether the transmitter is using a particular webhook. | ||||||
|  | func (t *Transmitter) HasWebhook(id string) bool { | ||||||
|  | 	t.mutex.RLock() | ||||||
|  | 	defer t.mutex.RUnlock() | ||||||
|  |  | ||||||
|  | 	for _, wh := range t.channelWebhooks { | ||||||
|  | 		if wh.ID == id { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddWebhook allows you to register a channel's webhook with the transmitter. | ||||||
|  | func (t *Transmitter) AddWebhook(channelID string, webhook *discordgo.Webhook) bool { | ||||||
|  | 	t.Log.Debugf("Manually added webhook %#v to channel %#v", webhook.ID, channelID) | ||||||
|  | 	t.mutex.Lock() | ||||||
|  | 	defer t.mutex.Unlock() | ||||||
|  |  | ||||||
|  | 	_, replaced := t.channelWebhooks[channelID] | ||||||
|  | 	t.channelWebhooks[channelID] = webhook | ||||||
|  | 	return replaced | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RefreshGuildWebhooks loads "relevant" webhooks into the transmitter, with careful permission handling. | ||||||
|  | // | ||||||
|  | // Notes: | ||||||
|  | // | ||||||
|  | // - A webhook is "relevant" if it was created by this bot -- the ApplicationID should match the bot's ID. | ||||||
|  | // - The term "having permission" means having the "Manage Webhooks" permission. See ErrPermissionDenied for more information. | ||||||
|  | // - This function is additive and will not unload previously loaded webhooks. | ||||||
|  | // - A nil channelIDs slice is treated the same as an empty one. | ||||||
|  | // | ||||||
|  | // If the bot has guild-wide permission: | ||||||
|  | // | ||||||
|  | // 1. it will load any "relevant" webhooks from the entire guild | ||||||
|  | // 2. the given slice is ignored | ||||||
|  | // | ||||||
|  | // If the bot does not have guild-wide permission: | ||||||
|  | // | ||||||
|  | // 1. it will load any "relevant" webhooks in each channel | ||||||
|  | // 2. a single error will be returned if any error occurs (incl. if there is no permission for any of these channels) | ||||||
|  | // | ||||||
|  | // If any channel has more than one "relevant" webhook, it will randomly pick one. | ||||||
|  | func (t *Transmitter) RefreshGuildWebhooks(channelIDs []string) error { | ||||||
|  | 	t.Log.Debugln("Refreshing guild webhooks") | ||||||
|  |  | ||||||
|  | 	botID, err := getDiscordUserID(t.session) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("could not get current user: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get all existing webhooks | ||||||
|  | 	hooks, err := t.session.GuildWebhooks(t.guild) | ||||||
|  | 	if err != nil { | ||||||
|  | 		switch { | ||||||
|  | 		case isDiscordPermissionError(err): | ||||||
|  | 			// We fallback on manually fetching hooks from individual channels | ||||||
|  | 			// if we don't have the "Manage Webhooks" permission globally. | ||||||
|  | 			// We can only do this if we were provided channelIDs, though. | ||||||
|  | 			if len(channelIDs) == 0 { | ||||||
|  | 				return ErrPermissionDenied | ||||||
|  | 			} | ||||||
|  | 			t.Log.Debugln("Missing global 'Manage Webhooks' permission, falling back on per-channel permission") | ||||||
|  | 			return t.fetchChannelsHooks(channelIDs, botID) | ||||||
|  | 		default: | ||||||
|  | 			return fmt.Errorf("could not get webhooks: %w", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.Log.Debugln("Refreshing guild webhooks using global permission") | ||||||
|  | 	t.assignHooksByAppID(hooks, botID, false) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // createWebhook creates a webhook for a specific channel. | ||||||
|  | func (t *Transmitter) createWebhook(channel string) (*discordgo.Webhook, error) { | ||||||
|  | 	t.mutex.Lock() | ||||||
|  | 	defer t.mutex.Unlock() | ||||||
|  |  | ||||||
|  | 	wh, err := t.session.WebhookCreate(channel, t.title+time.Now().Format(" 3:04:05PM"), "") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.channelWebhooks[channel] = wh | ||||||
|  | 	return wh, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *Transmitter) getWebhook(channel string) *discordgo.Webhook { | ||||||
|  | 	t.mutex.RLock() | ||||||
|  | 	defer t.mutex.RUnlock() | ||||||
|  |  | ||||||
|  | 	return t.channelWebhooks[channel] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *Transmitter) getOrCreateWebhook(channelID string) (*discordgo.Webhook, error) { | ||||||
|  | 	// If we have a webhook for this channel, immediately return it | ||||||
|  | 	wh := t.getWebhook(channelID) | ||||||
|  | 	if wh != nil { | ||||||
|  | 		return wh, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Early exit if we don't want to automatically create one | ||||||
|  | 	if !t.autoCreate { | ||||||
|  | 		return nil, ErrWebhookNotFound | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.Log.Infof("Creating a webhook for %s\n", channelID) | ||||||
|  | 	wh, err := t.createWebhook(channelID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("could not create webhook: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return wh, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // fetchChannelsHooks fetches hooks for the given channelIDs and calls assignHooksByAppID for each channel's hooks | ||||||
|  | func (t *Transmitter) fetchChannelsHooks(channelIDs []string, botID string) error { | ||||||
|  | 	// For each channel, search for relevant hooks | ||||||
|  | 	var failedHooks []string | ||||||
|  | 	for _, channelID := range channelIDs { | ||||||
|  | 		hooks, err := t.session.ChannelWebhooks(channelID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			failedHooks = append(failedHooks, "\n- "+channelID+": "+err.Error()) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		t.assignHooksByAppID(hooks, botID, true) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Compose an error if any hooks failed | ||||||
|  | 	if len(failedHooks) > 0 { | ||||||
|  | 		return errors.New("failed to fetch hooks:" + strings.Join(failedHooks, "")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *Transmitter) assignHooksByAppID(hooks []*discordgo.Webhook, appID string, channelTargeted bool) { | ||||||
|  | 	logLine := "Picking up webhook" | ||||||
|  | 	if channelTargeted { | ||||||
|  | 		logLine += " (channel targeted)" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.mutex.Lock() | ||||||
|  | 	defer t.mutex.Unlock() | ||||||
|  |  | ||||||
|  | 	for _, wh := range hooks { | ||||||
|  | 		if wh.ApplicationID != appID { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		t.channelWebhooks[wh.ChannelID] = wh | ||||||
|  | 		t.Log.WithFields(log.Fields{ | ||||||
|  | 			"id":      wh.ID, | ||||||
|  | 			"name":    wh.Name, | ||||||
|  | 			"channel": wh.ChannelID, | ||||||
|  | 		}).Println(logLine) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								bridge/discord/transmitter/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								bridge/discord/transmitter/utils.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | package transmitter | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/matterbridge/discordgo" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions | ||||||
|  | func isDiscordPermissionError(err error) bool { | ||||||
|  | 	if err == nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	restErr, ok := err.(*discordgo.RESTError) | ||||||
|  | 	if !ok { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return restErr.Message != nil && restErr.Message.Code == discordgo.ErrCodeMissingPermissions | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getDiscordUserID gets own user ID from state, and fallback on API request | ||||||
|  | func getDiscordUserID(session *discordgo.Session) (string, error) { | ||||||
|  | 	if user := session.State.User; user != nil { | ||||||
|  | 		return user.ID, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	user, err := session.User("@me") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return user.ID, nil | ||||||
|  | } | ||||||
							
								
								
									
										148
									
								
								bridge/discord/webhook.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								bridge/discord/webhook.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | |||||||
|  | package bdiscord | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  |  | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
|  | 	"github.com/matterbridge/discordgo" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks | ||||||
|  | func (b *Bdiscord) shouldMessageUseWebhooks(msg *config.Message) bool { | ||||||
|  | 	if b.useAutoWebhooks { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.channelsMutex.RLock() | ||||||
|  | 	defer b.channelsMutex.RUnlock() | ||||||
|  | 	if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok { | ||||||
|  | 		if ci.Options.WebhookURL != "" { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // maybeGetLocalAvatar checks if UseLocalAvatar contains the message's | ||||||
|  | // account or protocol, and if so, returns the Discord avatar (if exists) | ||||||
|  | func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string { | ||||||
|  | 	for _, val := range b.GetStringSlice("UseLocalAvatar") { | ||||||
|  | 		if msg.Protocol != val && msg.Account != val { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		member, err := b.getGuildMemberByNick(msg.Username) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "" | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return member.User.AvatarURL("") | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // webhookSend send one or more message via webhook, taking care of file | ||||||
|  | // uploads (from slack, telegram or mattermost). | ||||||
|  | // Returns messageID and error. | ||||||
|  | func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) { | ||||||
|  | 	var ( | ||||||
|  | 		res *discordgo.Message | ||||||
|  | 		err error | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	// If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this) | ||||||
|  | 	if msg.Avatar == "" { | ||||||
|  | 		msg.Avatar = b.maybeGetLocalAvatar(msg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// WebhookParams can have either `Content` or `File`. | ||||||
|  |  | ||||||
|  | 	// We can't send empty messages. | ||||||
|  | 	if msg.Text != "" { | ||||||
|  | 		res, err = b.transmitter.Send( | ||||||
|  | 			channelID, | ||||||
|  | 			&discordgo.WebhookParams{ | ||||||
|  | 				Content:         msg.Text, | ||||||
|  | 				Username:        msg.Username, | ||||||
|  | 				AvatarURL:       msg.Avatar, | ||||||
|  | 				AllowedMentions: b.getAllowedMentions(), | ||||||
|  | 			}, | ||||||
|  | 		) | ||||||
|  | 		if err != nil { | ||||||
|  | 			b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if msg.Extra != nil { | ||||||
|  | 		for _, f := range msg.Extra["file"] { | ||||||
|  | 			fi := f.(config.FileInfo) | ||||||
|  | 			file := discordgo.File{ | ||||||
|  | 				Name:        fi.Name, | ||||||
|  | 				ContentType: "", | ||||||
|  | 				Reader:      bytes.NewReader(*fi.Data), | ||||||
|  | 			} | ||||||
|  | 			content := fi.Comment | ||||||
|  |  | ||||||
|  | 			_, e2 := b.transmitter.Send( | ||||||
|  | 				channelID, | ||||||
|  | 				&discordgo.WebhookParams{ | ||||||
|  | 					Username:        msg.Username, | ||||||
|  | 					AvatarURL:       msg.Avatar, | ||||||
|  | 					File:            &file, | ||||||
|  | 					Content:         content, | ||||||
|  | 					AllowedMentions: b.getAllowedMentions(), | ||||||
|  | 				}, | ||||||
|  | 			) | ||||||
|  | 			if e2 != nil { | ||||||
|  | 				b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return res, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (string, error) { | ||||||
|  | 	// skip events | ||||||
|  | 	if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange { | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// skip empty messages | ||||||
|  | 	if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) { | ||||||
|  | 		b.Log.Debugf("Skipping empty message %#v", msg) | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped")) | ||||||
|  | 	msg.Text = b.replaceUserMentions(msg.Text) | ||||||
|  | 	// discord username must be [0..32] max | ||||||
|  | 	if len(msg.Username) > 32 { | ||||||
|  | 		msg.Username = msg.Username[0:32] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if msg.ID != "" { | ||||||
|  | 		b.Log.Debugf("Editing webhook message") | ||||||
|  | 		err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{ | ||||||
|  | 			Content:         msg.Text, | ||||||
|  | 			Username:        msg.Username, | ||||||
|  | 			AllowedMentions: b.getAllowedMentions(), | ||||||
|  | 		}) | ||||||
|  | 		if err == nil { | ||||||
|  | 			return msg.ID, nil | ||||||
|  | 		} | ||||||
|  | 		b.Log.Errorf("Could not edit webhook message: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("Processing webhook sending for message %#v", msg) | ||||||
|  | 	discordMsg, err := b.webhookSend(msg, channelID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msg, err) | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	if discordMsg == nil { | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return discordMsg.ID, nil | ||||||
|  | } | ||||||
							
								
								
									
										252
									
								
								bridge/harmony/harmony.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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 | ||||||
|  | } | ||||||
| @@ -15,6 +15,7 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/gomarkdown/markdown" | 	"github.com/gomarkdown/markdown" | ||||||
|  | 	"github.com/gomarkdown/markdown/html" | ||||||
| 	"github.com/gomarkdown/markdown/parser" | 	"github.com/gomarkdown/markdown/parser" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
| @@ -47,6 +48,30 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) { | |||||||
| 	return &data, nil | 	return &data, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // DownloadFileAuthRocket downloads the given URL using the specified Rocket user ID and authentication token. | ||||||
|  | func DownloadFileAuthRocket(url, token, userID string) (*[]byte, error) { | ||||||
|  | 	var buf bytes.Buffer | ||||||
|  | 	client := &http.Client{ | ||||||
|  | 		Timeout: time.Second * 5, | ||||||
|  | 	} | ||||||
|  | 	req, err := http.NewRequest("GET", url, nil) | ||||||
|  |  | ||||||
|  | 	req.Header.Add("X-Auth-Token", token) | ||||||
|  | 	req.Header.Add("X-User-Id", userID) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	resp, err := client.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 	_, err = io.Copy(&buf, resp.Body) | ||||||
|  | 	data := buf.Bytes() | ||||||
|  | 	return &data, err | ||||||
|  | } | ||||||
|  |  | ||||||
| // GetSubLines splits messages in newline-delimited lines. If maxLineLength is | // GetSubLines splits messages in newline-delimited lines. If maxLineLength is | ||||||
| // specified as non-zero GetSubLines will also clip long lines to the maximum | // specified as non-zero GetSubLines will also clip long lines to the maximum | ||||||
| // length and insert a warning marker that the line was clipped. | // length and insert a warning marker that the line was clipped. | ||||||
| @@ -54,9 +79,7 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) { | |||||||
| // TODO: The current implementation has the inconvenient that it disregards | // TODO: The current implementation has the inconvenient that it disregards | ||||||
| // word boundaries when splitting but this is hard to solve without potentially | // word boundaries when splitting but this is hard to solve without potentially | ||||||
| // breaking formatting and other stylistic effects. | // breaking formatting and other stylistic effects. | ||||||
| func GetSubLines(message string, maxLineLength int) []string { | func GetSubLines(message string, maxLineLength int, clippingMessage string) []string { | ||||||
| 	const clippingMessage = " <clipped message>" |  | ||||||
|  |  | ||||||
| 	var lines []string | 	var lines []string | ||||||
| 	for _, line := range strings.Split(strings.TrimSpace(message), "\n") { | 	for _, line := range strings.Split(strings.TrimSpace(message), "\n") { | ||||||
| 		if maxLineLength == 0 || len([]byte(line)) <= maxLineLength { | 		if maxLineLength == 0 || len([]byte(line)) <= maxLineLength { | ||||||
| @@ -71,8 +94,8 @@ func GetSubLines(message string, maxLineLength int) []string { | |||||||
| 		var splitStart int | 		var splitStart int | ||||||
| 		var startOfPreviousRune int | 		var startOfPreviousRune int | ||||||
| 		for i := range line { | 		for i := range line { | ||||||
| 			if i-splitStart > maxLineLength-len([]byte(clippingMessage)) { | 			if i-splitStart > maxLineLength { | ||||||
| 				lines = append(lines, line[splitStart:startOfPreviousRune]+clippingMessage) | 				lines = append(lines, line[splitStart:startOfPreviousRune]) | ||||||
| 				splitStart = startOfPreviousRune | 				splitStart = startOfPreviousRune | ||||||
| 			} | 			} | ||||||
| 			startOfPreviousRune = i | 			startOfPreviousRune = i | ||||||
| @@ -141,17 +164,23 @@ func HandleDownloadSize(logger *logrus.Entry, msg *config.Message, name string, | |||||||
|  |  | ||||||
| // HandleDownloadData adds the data for a remote file into a Matterbridge gateway message. | // 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) { | 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 | 	var avatar bool | ||||||
| 	logger.Debugf("Download OK %#v %#v", name, len(*data)) | 	logger.Debugf("Download OK %#v %#v", name, len(*data)) | ||||||
| 	if msg.Event == config.EventAvatarDownload { | 	if msg.Event == config.EventAvatarDownload { | ||||||
| 		avatar = true | 		avatar = true | ||||||
| 	} | 	} | ||||||
| 	msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{ | 	msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{ | ||||||
| 		Name:    name, | 		Name:     name, | ||||||
| 		Data:    data, | 		Data:     data, | ||||||
| 		URL:     url, | 		URL:      url, | ||||||
| 		Comment: comment, | 		Comment:  comment, | ||||||
| 		Avatar:  avatar, | 		Avatar:   avatar, | ||||||
|  | 		NativeID: id, | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -165,8 +194,11 @@ func RemoveEmptyNewLines(msg string) string { | |||||||
|  |  | ||||||
| // ClipMessage trims a message to the specified length if it exceeds it and adds a warning | // ClipMessage trims a message to the specified length if it exceeds it and adds a warning | ||||||
| // to the message in case it does so. | // to the message in case it does so. | ||||||
| func ClipMessage(text string, length int) string { | func ClipMessage(text string, length int, clippingMessage string) string { | ||||||
| 	const clippingMessage = " <clipped message>" | 	if clippingMessage == "" { | ||||||
|  | 		clippingMessage = " <clipped message>" | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if len(text) > length { | 	if len(text) > length { | ||||||
| 		text = text[:length-len(clippingMessage)] | 		text = text[:length-len(clippingMessage)] | ||||||
| 		if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError { | 		if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError { | ||||||
| @@ -179,16 +211,19 @@ func ClipMessage(text string, length int) string { | |||||||
|  |  | ||||||
| // ParseMarkdown takes in an input string as markdown and parses it to html | // ParseMarkdown takes in an input string as markdown and parses it to html | ||||||
| func ParseMarkdown(input string) string { | func ParseMarkdown(input string) string { | ||||||
| 	extensions := parser.HardLineBreak | 	extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode | ||||||
| 	markdownParser := parser.NewWithExtensions(extensions) | 	markdownParser := parser.NewWithExtensions(extensions) | ||||||
| 	parsedMarkdown := markdown.ToHTML([]byte(input), markdownParser, nil) | 	renderer := html.NewRenderer(html.RendererOptions{ | ||||||
|  | 		Flags: 0, | ||||||
|  | 	}) | ||||||
|  | 	parsedMarkdown := markdown.ToHTML([]byte(input), markdownParser, renderer) | ||||||
| 	res := string(parsedMarkdown) | 	res := string(parsedMarkdown) | ||||||
| 	res = strings.TrimPrefix(res, "<p>") | 	res = strings.TrimPrefix(res, "<p>") | ||||||
| 	res = strings.TrimSuffix(res, "</p>\n") | 	res = strings.TrimSuffix(res, "</p>\n") | ||||||
| 	return res | 	return res | ||||||
| } | } | ||||||
|  |  | ||||||
| // ConvertWebPToPNG 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 { | func ConvertWebPToPNG(data *[]byte) error { | ||||||
| 	r := bytes.NewReader(*data) | 	r := bytes.NewReader(*data) | ||||||
| 	m, err := webp.Decode(r) | 	m, err := webp.Decode(r) | ||||||
|   | |||||||
| @@ -10,98 +10,96 @@ import ( | |||||||
|  |  | ||||||
| const testLineLength = 64 | const testLineLength = 64 | ||||||
|  |  | ||||||
| var ( | var lineSplittingTestCases = map[string]struct { | ||||||
| 	lineSplittingTestCases = map[string]struct { | 	input          string | ||||||
| 		input          string | 	splitOutput    []string | ||||||
| 		splitOutput    []string | 	nonSplitOutput []string | ||||||
| 		nonSplitOutput []string | }{ | ||||||
| 	}{ | 	"Short single-line message": { | ||||||
| 		"Short single-line message": { | 		input:          "short", | ||||||
| 			input:          "short", | 		splitOutput:    []string{"short"}, | ||||||
| 			splitOutput:    []string{"short"}, | 		nonSplitOutput: []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": { | 		nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}, | ||||||
| 			input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", | 	}, | ||||||
| 			splitOutput: []string{ | 	"Short multi-line message": { | ||||||
| 				"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>", | 		input: "I\ncan't\nget\nno\nsatisfaction!", | ||||||
| 				"cing elit, sed do eiusmod tempor incididunt ut <clipped message>", | 		splitOutput: []string{ | ||||||
| 				" labore et dolore magna aliqua.", | 			"I", | ||||||
| 			}, | 			"can't", | ||||||
| 			nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}, | 			"get", | ||||||
|  | 			"no", | ||||||
|  | 			"satisfaction!", | ||||||
| 		}, | 		}, | ||||||
| 		"Short multi-line message": { | 		nonSplitOutput: []string{ | ||||||
| 			input: "I\ncan't\nget\nno\nsatisfaction!", | 			"I", | ||||||
| 			splitOutput: []string{ | 			"can't", | ||||||
| 				"I", | 			"get", | ||||||
| 				"can't", | 			"no", | ||||||
| 				"get", | 			"satisfaction!", | ||||||
| 				"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" + | 	"Long multi-line message": { | ||||||
| 				"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" + | 		input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" + | ||||||
| 				"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" + | 			"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" + | ||||||
| 				"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", | 			"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" + | ||||||
| 			splitOutput: []string{ | 			"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", | ||||||
| 				"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>", | 		splitOutput: []string{ | ||||||
| 				"cing elit, sed do eiusmod tempor incididunt ut <clipped message>", | 			"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>", | ||||||
| 				" labore et dolore magna aliqua.", | 			"cing elit, sed do eiusmod tempor incididunt ut <clipped message>", | ||||||
| 				"Ut enim ad minim veniam, quis nostrud exercita <clipped message>", | 			" labore et dolore magna aliqua.", | ||||||
| 				"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>", | 			"Ut enim ad minim veniam, quis nostrud exercita <clipped message>", | ||||||
| 				"modo consequat.", | 			"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>", | ||||||
| 				"Duis aute irure dolor in reprehenderit in volu <clipped message>", | 			"modo consequat.", | ||||||
| 				"ptate velit esse cillum dolore eu fugiat nulla <clipped message>", | 			"Duis aute irure dolor in reprehenderit in volu <clipped message>", | ||||||
| 				" pariatur.", | 			"ptate velit esse cillum dolore eu fugiat nulla <clipped message>", | ||||||
| 				"Excepteur sint occaecat cupidatat non proident <clipped message>", | 			" pariatur.", | ||||||
| 				", sunt in culpa qui officia deserunt mollit an <clipped message>", | 			"Excepteur sint occaecat cupidatat non proident <clipped message>", | ||||||
| 				"im id est laborum.", | 			", 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.", |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		"Message ending with new-line.": { | 		nonSplitOutput: []string{ | ||||||
| 			input:          "Newline ending\n", | 			"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", | ||||||
| 			splitOutput:    []string{"Newline ending"}, | 			"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", | ||||||
| 			nonSplitOutput: []string{"Newline ending"}, | 			"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: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說", | 	"Message ending with new-line.": { | ||||||
| 			splitOutput: []string{ | 		input:          "Newline ending\n", | ||||||
| 				"不布人個我此而及單石業喜資富下 <clipped message>", | 		splitOutput:    []string{"Newline ending"}, | ||||||
| 				"我河下日沒一我臺空達的常景便物 <clipped message>", | 		nonSplitOutput: []string{"Newline ending"}, | ||||||
| 				"沒為……子大我別名解成?生賣的 <clipped message>", | 	}, | ||||||
| 				"全直黑,我自我結毛分洲了世當, <clipped message>", | 	"Long message containing UTF-8 multi-byte runes": { | ||||||
| 				"是政福那是東;斯說", | 		input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說", | ||||||
| 			}, | 		splitOutput: []string{ | ||||||
| 			nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"}, | 			"不布人個我此而及單石業喜資富下 <clipped message>", | ||||||
|  | 			"我河下日沒一我臺空達的常景便物 <clipped message>", | ||||||
|  | 			"沒為……子大我別名解成?生賣的 <clipped message>", | ||||||
|  | 			"全直黑,我自我結毛分洲了世當, <clipped message>", | ||||||
|  | 			"是政福那是東;斯說", | ||||||
| 		}, | 		}, | ||||||
| 	} | 		nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"}, | ||||||
| ) | 	}, | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestGetSubLines(t *testing.T) { | func TestGetSubLines(t *testing.T) { | ||||||
| 	for testname, testcase := range lineSplittingTestCases { | 	for testname, testcase := range lineSplittingTestCases { | ||||||
| 		splitLines := GetSubLines(testcase.input, testLineLength) | 		splitLines := GetSubLines(testcase.input, testLineLength, "") | ||||||
| 		assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname) | 		assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname) | ||||||
| 		for _, splitLine := range splitLines { | 		for _, splitLine := range splitLines { | ||||||
| 			byteLength := len([]byte(splitLine)) | 			byteLength := len([]byte(splitLine)) | ||||||
| 			assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength) | 			assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		nonSplitLines := GetSubLines(testcase.input, 0) | 		nonSplitLines := GetSubLines(testcase.input, 0, "") | ||||||
| 		assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname) | 		assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -110,16 +108,19 @@ func TestConvertWebPToPNG(t *testing.T) { | |||||||
| 	if os.Getenv("LOCAL_TEST") == "" { | 	if os.Getenv("LOCAL_TEST") == "" { | ||||||
| 		t.Skip() | 		t.Skip() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	input, err := ioutil.ReadFile("test.webp") | 	input, err := ioutil.ReadFile("test.webp") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fail() | 		t.Fail() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	d := &input | 	d := &input | ||||||
| 	err = ConvertWebPToPNG(d) | 	err = ConvertWebPToPNG(d) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fail() | 		t.Fail() | ||||||
| 	} | 	} | ||||||
| 	err = ioutil.WriteFile("test.png", *d, 0644) |  | ||||||
|  | 	err = ioutil.WriteFile("test.png", *d, 0o644) // nolint:gosec | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fail() | 		t.Fail() | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										36
									
								
								bridge/helper/libtgsconverter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								bridge/helper/libtgsconverter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | //go:build cgo | ||||||
|  | // +build cgo | ||||||
|  |  | ||||||
|  | package helper | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"github.com/Benau/tgsconverter/libtgsconverter" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func CanConvertTgsToX() error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ConvertTgsToX convert input data (which should be tgs format) to any format supported by libtgsconverter | ||||||
|  | func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error { | ||||||
|  | 	options := libtgsconverter.NewConverterOptions() | ||||||
|  | 	options.SetExtension(outputFormat) | ||||||
|  | 	blob, err := libtgsconverter.ImportFromData(*data, options) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to run libtgsconverter.ImportFromData: %s", err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	*data = blob | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func SupportsFormat(format string) bool { | ||||||
|  | 	return libtgsconverter.SupportsExtension(format) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func LottieBackend() string { | ||||||
|  | 	return "libtgsconverter" | ||||||
|  | } | ||||||
							
								
								
									
										89
									
								
								bridge/helper/lottie_convert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								bridge/helper/lottie_convert.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | // +build !cgo | ||||||
|  |  | ||||||
|  | package helper | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"os/exec" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // CanConvertTgsToX Checks whether the external command necessary for ConvertTgsToX works. | ||||||
|  | func CanConvertTgsToX() error { | ||||||
|  | 	// We depend on the fact that `lottie_convert.py --help` has exit status 0. | ||||||
|  | 	// Hyrum's Law predicted this, and Murphy's Law predicts that this will break eventually. | ||||||
|  | 	// However, there is no alternative like `lottie_convert.py --is-properly-installed` | ||||||
|  | 	cmd := exec.Command("lottie_convert.py", "--help") | ||||||
|  | 	return cmd.Run() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ConvertTgsToWebP convert input data (which should be tgs format) to WebP format | ||||||
|  | // This relies on an external command, which is ugly, but works. | ||||||
|  | func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error { | ||||||
|  | 	// lottie can't handle input from a pipe, so write to a temporary file: | ||||||
|  | 	tmpInFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-input-*.tgs") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	tmpInFileName := tmpInFile.Name() | ||||||
|  | 	defer func() { | ||||||
|  | 		if removeErr := os.Remove(tmpInFileName); removeErr != nil { | ||||||
|  | 			logger.Errorf("Could not delete temporary (input) file %s: %v", tmpInFileName, removeErr) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 	// lottie can handle writing to a pipe, but there is no way to do that platform-independently. | ||||||
|  | 	// "/dev/stdout" won't work on Windows, and "-" upsets Cairo for some reason. So we need another file: | ||||||
|  | 	tmpOutFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-output-*.data") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	tmpOutFileName := tmpOutFile.Name() | ||||||
|  | 	defer func() { | ||||||
|  | 		if removeErr := os.Remove(tmpOutFileName); removeErr != nil { | ||||||
|  | 			logger.Errorf("Could not delete temporary (output) file %s: %v", tmpOutFileName, removeErr) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	if _, writeErr := tmpInFile.Write(*data); writeErr != nil { | ||||||
|  | 		return writeErr | ||||||
|  | 	} | ||||||
|  | 	// Must close before calling lottie to avoid data races: | ||||||
|  | 	if closeErr := tmpInFile.Close(); closeErr != nil { | ||||||
|  | 		return closeErr | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Call lottie to transform: | ||||||
|  | 	cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpInFileName, tmpOutFileName) | ||||||
|  | 	cmd.Stdout = nil | ||||||
|  | 	cmd.Stderr = nil | ||||||
|  | 	// NB: lottie writes progress into to stderr in all cases. | ||||||
|  | 	_, stderr := cmd.Output() | ||||||
|  | 	if stderr != nil { | ||||||
|  | 		// 'stderr' already contains some parts of Stderr, because it was set to 'nil'. | ||||||
|  | 		return stderr | ||||||
|  | 	} | ||||||
|  | 	dataContents, err := ioutil.ReadFile(tmpOutFileName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	*data = dataContents | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func SupportsFormat(format string) bool { | ||||||
|  | 	switch format { | ||||||
|  | 	case "png": | ||||||
|  | 		fallthrough | ||||||
|  | 	case "webp": | ||||||
|  | 		return true | ||||||
|  | 	default: | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func LottieBackend() string { | ||||||
|  | 	return "lottie_convert.py" | ||||||
|  | } | ||||||
| @@ -10,8 +10,8 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/dfordsoft/golib/ic" |  | ||||||
| 	"github.com/lrstanley/girc" | 	"github.com/lrstanley/girc" | ||||||
|  | 	"github.com/missdeer/golib/ic" | ||||||
| 	"github.com/paulrosania/go-charset/charset" | 	"github.com/paulrosania/go-charset/charset" | ||||||
| 	"github.com/saintfish/chardet" | 	"github.com/saintfish/chardet" | ||||||
|  |  | ||||||
| @@ -54,12 +54,12 @@ func (b *Birc) handleFiles(msg *config.Message) bool { | |||||||
| 	for _, f := range msg.Extra["file"] { | 	for _, f := range msg.Extra["file"] { | ||||||
| 		fi := f.(config.FileInfo) | 		fi := f.(config.FileInfo) | ||||||
| 		if fi.Comment != "" { | 		if fi.Comment != "" { | ||||||
| 			msg.Text += fi.Comment + ": " | 			msg.Text += fi.Comment + " : " | ||||||
| 		} | 		} | ||||||
| 		if fi.URL != "" { | 		if fi.URL != "" { | ||||||
| 			msg.Text = fi.URL | 			msg.Text = fi.URL | ||||||
| 			if fi.Comment != "" { | 			if fi.Comment != "" { | ||||||
| 				msg.Text = fi.Comment + ": " + fi.URL | 				msg.Text = fi.Comment + " : " + fi.URL | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event} | 		b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event} | ||||||
| @@ -67,6 +67,20 @@ func (b *Birc) handleFiles(msg *config.Message) bool { | |||||||
| 	return true | 	return true | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (b *Birc) handleInvite(client *girc.Client, event girc.Event) { | ||||||
|  | 	if len(event.Params) != 2 { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	channel := event.Params[1] | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("got invite for %s", channel) | ||||||
|  |  | ||||||
|  | 	if _, ok := b.channels[channel]; ok { | ||||||
|  | 		b.i.Cmd.Join(channel) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) { | func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) { | ||||||
| 	if len(event.Params) == 0 { | 	if len(event.Params) == 0 { | ||||||
| 		b.Log.Debugf("handleJoinPart: empty Params? %#v", event) | 		b.Log.Debugf("handleJoinPart: empty Params? %#v", event) | ||||||
| @@ -109,14 +123,15 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) { | |||||||
| 	i := b.i | 	i := b.i | ||||||
| 	b.Nick = event.Params[0] | 	b.Nick = event.Params[0] | ||||||
|  |  | ||||||
| 	i.Handlers.Add("PRIVMSG", b.handlePrivMsg) | 	i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg) | ||||||
| 	i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg) | 	i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg) | ||||||
| 	i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) | 	i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) | ||||||
| 	i.Handlers.Add(girc.NOTICE, b.handleNotice) | 	i.Handlers.AddBg(girc.NOTICE, b.handleNotice) | ||||||
| 	i.Handlers.Add("JOIN", b.handleJoinPart) | 	i.Handlers.AddBg("JOIN", b.handleJoinPart) | ||||||
| 	i.Handlers.Add("PART", b.handleJoinPart) | 	i.Handlers.AddBg("PART", b.handleJoinPart) | ||||||
| 	i.Handlers.Add("QUIT", b.handleJoinPart) | 	i.Handlers.AddBg("QUIT", b.handleJoinPart) | ||||||
| 	i.Handlers.Add("KICK", b.handleJoinPart) | 	i.Handlers.AddBg("KICK", b.handleJoinPart) | ||||||
|  | 	i.Handlers.Add("INVITE", b.handleInvite) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Birc) handleNickServ() { | func (b *Birc) handleNickServ() { | ||||||
| @@ -170,7 +185,14 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { | |||||||
| 	if b.skipPrivMsg(event) { | 	if b.skipPrivMsg(event) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host} |  | ||||||
|  | 	rmsg := config.Message{ | ||||||
|  | 		Username: event.Source.Name, | ||||||
|  | 		Channel:  strings.ToLower(event.Params[0]), | ||||||
|  | 		Account:  b.Account, | ||||||
|  | 		UserID:   event.Source.Ident + "@" + event.Source.Host, | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event) | 	b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event) | ||||||
|  |  | ||||||
| 	// set action event | 	// set action event | ||||||
| @@ -178,6 +200,11 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { | |||||||
| 		rmsg.Event = config.EventUserAction | 		rmsg.Event = config.EventUserAction | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// set NOTICE event | ||||||
|  | 	if event.Command == "NOTICE" { | ||||||
|  | 		rmsg.Event = config.EventNoticeIRC | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// strip action, we made an event if it was an action | 	// strip action, we made an event if it was an action | ||||||
| 	rmsg.Text += event.StripAction() | 	rmsg.Text += event.StripAction() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,8 +2,10 @@ package birc | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"crypto/tls" | 	"crypto/tls" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"hash/crc32" | 	"hash/crc32" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"net" | 	"net" | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| @@ -14,6 +16,7 @@ import ( | |||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/lrstanley/girc" | 	"github.com/lrstanley/girc" | ||||||
|  | 	stripmd "github.com/writeas/go-strip-markdown" | ||||||
|  |  | ||||||
| 	// We need to import the 'data' package as an implicit dependency. | 	// We need to import the 'data' package as an implicit dependency. | ||||||
| 	// See: https://godoc.org/github.com/paulrosania/go-charset/charset | 	// See: https://godoc.org/github.com/paulrosania/go-charset/charset | ||||||
| @@ -22,12 +25,13 @@ import ( | |||||||
|  |  | ||||||
| type Birc struct { | type Birc struct { | ||||||
| 	i                                         *girc.Client | 	i                                         *girc.Client | ||||||
| 	Nick                                      string | 	Nick, MessageClipped                      string | ||||||
| 	names                                     map[string][]string | 	names                                     map[string][]string | ||||||
| 	connected                                 chan error | 	connected                                 chan error | ||||||
| 	Local                                     chan config.Message // local queue for flood control | 	Local                                     chan config.Message // local queue for flood control | ||||||
| 	FirstConnection, authDone                 bool | 	FirstConnection, authDone                 bool | ||||||
| 	MessageDelay, MessageQueue, MessageLength int | 	MessageDelay, MessageQueue, MessageLength int | ||||||
|  | 	channels                                  map[string]bool | ||||||
|  |  | ||||||
| 	*bridge.Config | 	*bridge.Config | ||||||
| } | } | ||||||
| @@ -38,6 +42,8 @@ func New(cfg *bridge.Config) bridge.Bridger { | |||||||
| 	b.Nick = b.GetString("Nick") | 	b.Nick = b.GetString("Nick") | ||||||
| 	b.names = make(map[string][]string) | 	b.names = make(map[string][]string) | ||||||
| 	b.connected = make(chan error) | 	b.connected = make(chan error) | ||||||
|  | 	b.channels = make(map[string]bool) | ||||||
|  |  | ||||||
| 	if b.GetInt("MessageDelay") == 0 { | 	if b.GetInt("MessageDelay") == 0 { | ||||||
| 		b.MessageDelay = 1300 | 		b.MessageDelay = 1300 | ||||||
| 	} else { | 	} else { | ||||||
| @@ -67,6 +73,10 @@ func (b *Birc) Command(msg *config.Message) string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Birc) Connect() error { | 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.Local = make(chan config.Message, b.MessageQueue+10) | ||||||
| 	b.Log.Infof("Connecting %s", b.GetString("Server")) | 	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 { | func (b *Birc) JoinChannel(channel config.ChannelInfo) error { | ||||||
|  | 	b.channels[channel.Name] = true | ||||||
| 	// need to check if we have nickserv auth done before joining channels | 	// need to check if we have nickserv auth done before joining channels | ||||||
| 	for { | 	for { | ||||||
| 		if b.authDone { | 		if b.authDone { | ||||||
| @@ -156,23 +167,24 @@ func (b *Birc) Send(msg config.Message) (string, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var msgLines []string | 	var msgLines []string | ||||||
| 	if b.GetBool("MessageSplit") { | 	if b.GetBool("StripMarkdown") { | ||||||
| 		msgLines = helper.GetSubLines(msg.Text, b.MessageLength) | 		msg.Text = stripmd.Strip(msg.Text) | ||||||
| 	} else { |  | ||||||
| 		msgLines = helper.GetSubLines(msg.Text, 0) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if b.GetBool("MessageSplit") { | ||||||
|  | 		msgLines = helper.GetSubLines(msg.Text, b.MessageLength, "") | ||||||
|  | 	} else { | ||||||
|  | 		msgLines = []string{helper.GetSubLines(msg.Text, b.MessageLength, "")[0] + b.getMessageClipped()} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	for i := range msgLines { | 	for i := range msgLines { | ||||||
| 		if len(b.Local) >= b.MessageQueue { | 		if len(b.Local) >= b.MessageQueue { | ||||||
| 			b.Log.Debugf("flooding, dropping message (queue at %d)", len(b.Local)) | 			b.Log.Debugf("flooding, dropping message (queue at %d)", len(b.Local)) | ||||||
| 			return "", nil | 			return "", nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		b.Local <- config.Message{ | 		msg.Text = msgLines[i] | ||||||
| 			Text:     msgLines[i], | 		b.Local <- msg | ||||||
| 			Username: msg.Username, |  | ||||||
| 			Channel:  msg.Channel, |  | ||||||
| 			Event:    msg.Event, |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 	return "", nil | 	return "", nil | ||||||
| } | } | ||||||
| @@ -199,22 +211,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() { | func (b *Birc) doSend() { | ||||||
| 	rate := time.Millisecond * time.Duration(b.MessageDelay) | 	rate := time.Millisecond * time.Duration(b.MessageDelay) | ||||||
| 	throttle := time.NewTicker(rate) | 	throttle := time.NewTicker(rate) | ||||||
| 	for msg := range b.Local { | 	for msg := range b.Local { | ||||||
| 		<-throttle.C | 		<-throttle.C | ||||||
| 		username := msg.Username | 		username := msg.Username | ||||||
| 		if b.GetBool("Colornicks") { | 		// Optional support for the proposed RELAYMSG extension, described at | ||||||
| 			checksum := crc32.ChecksumIEEE([]byte(msg.Username)) | 		// https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md | ||||||
| 			colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes | 		// nolint:nestif | ||||||
| 			username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) | 		if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) && | ||||||
| 		} | 			b.GetBool("UseRelayMsg") { | ||||||
| 		if msg.Event == config.EventUserAction { | 			username = sanitizeNick(username) | ||||||
| 			b.i.Cmd.Action(msg.Channel, username+msg.Text) | 			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 { | 		} else { | ||||||
| 			b.Log.Debugf("Sending to channel %s", msg.Channel) | 			if b.GetBool("Colornicks") { | ||||||
| 			b.i.Cmd.Message(msg.Channel, username+msg.Text) | 				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) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -229,8 +277,11 @@ func (b *Birc) getClient() (*girc.Client, error) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | 	user := b.GetString("UserName") | ||||||
|  | 	if user == "" { | ||||||
|  | 		user = b.GetString("Nick") | ||||||
|  | 	} | ||||||
| 	// fix strict user handling of girc | 	// fix strict user handling of girc | ||||||
| 	user := b.GetString("Nick") |  | ||||||
| 	for !girc.IsValidUser(user) { | 	for !girc.IsValidUser(user) { | ||||||
| 		if len(user) == 1 || len(user) == 0 { | 		if len(user) == 1 || len(user) == 0 { | ||||||
| 			user = "matterbridge" | 			user = "matterbridge" | ||||||
| @@ -238,6 +289,27 @@ func (b *Birc) getClient() (*girc.Client, error) { | |||||||
| 		} | 		} | ||||||
| 		user = user[1:] | 		user = user[1:] | ||||||
| 	} | 	} | ||||||
|  | 	realName := b.GetString("RealName") | ||||||
|  | 	if realName == "" { | ||||||
|  | 		realName = b.GetString("Nick") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	debug := ioutil.Discard | ||||||
|  | 	if b.GetInt("DebugLevel") == 2 { | ||||||
|  | 		debug = b.Log.Writer() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pingDelay, err := time.ParseDuration(b.GetString("pingdelay")) | ||||||
|  | 	if err != nil || pingDelay == 0 { | ||||||
|  | 		pingDelay = time.Minute | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("setting pingdelay to %s", pingDelay) | ||||||
|  |  | ||||||
|  | 	tlsConfig, err := b.getTLSConfig() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	i := girc.New(girc.Config{ | 	i := girc.New(girc.Config{ | ||||||
| 		Server:     server, | 		Server:     server, | ||||||
| @@ -245,10 +317,15 @@ func (b *Birc) getClient() (*girc.Client, error) { | |||||||
| 		Port:       port, | 		Port:       port, | ||||||
| 		Nick:       b.GetString("Nick"), | 		Nick:       b.GetString("Nick"), | ||||||
| 		User:       user, | 		User:       user, | ||||||
| 		Name:       b.GetString("Nick"), | 		Name:       realName, | ||||||
| 		SSL:        b.GetBool("UseTLS"), | 		SSL:        b.GetBool("UseTLS"), | ||||||
| 		TLSConfig:  &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec | 		Bind:       b.GetString("Bind"), | ||||||
| 		PingDelay:  time.Minute, | 		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 | 	return i, nil | ||||||
| } | } | ||||||
| @@ -258,12 +335,16 @@ func (b *Birc) endNames(client *girc.Client, event girc.Event) { | |||||||
| 	sort.Strings(b.names[channel]) | 	sort.Strings(b.names[channel]) | ||||||
| 	maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow() | 	maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow() | ||||||
| 	for len(b.names[channel]) > maxNamesPerPost { | 	for len(b.names[channel]) > maxNamesPerPost { | ||||||
| 		b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]), | 		b.Remote <- config.Message{ | ||||||
| 			Channel: channel, Account: b.Account} | 			Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]), | ||||||
|  | 			Channel: channel, Account: b.Account, | ||||||
|  | 		} | ||||||
| 		b.names[channel] = b.names[channel][maxNamesPerPost:] | 		b.names[channel] = b.names[channel][maxNamesPerPost:] | ||||||
| 	} | 	} | ||||||
| 	b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel]), | 	b.Remote <- config.Message{ | ||||||
| 		Channel: channel, Account: b.Account} | 		Username: b.Nick, Text: b.formatnicks(b.names[channel]), | ||||||
|  | 		Channel: channel, Account: b.Account, | ||||||
|  | 	} | ||||||
| 	b.names[channel] = nil | 	b.names[channel] = nil | ||||||
| 	b.i.Handlers.Clear(girc.RPL_NAMREPLY) | 	b.i.Handlers.Clear(girc.RPL_NAMREPLY) | ||||||
| 	b.i.Handlers.Clear(girc.RPL_ENDOFNAMES) | 	b.i.Handlers.Clear(girc.RPL_ENDOFNAMES) | ||||||
| @@ -274,7 +355,7 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool { | |||||||
| 	b.Nick = b.i.GetNick() | 	b.Nick = b.i.GetNick() | ||||||
|  |  | ||||||
| 	// freenode doesn't send 001 as first reply | 	// freenode doesn't send 001 as first reply | ||||||
| 	if event.Command == "NOTICE" { | 	if event.Command == "NOTICE" && len(event.Params) != 2 { | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 	// don't forward queries to the bot | 	// don't forward queries to the bot | ||||||
| @@ -285,6 +366,15 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool { | |||||||
| 	if event.Source.Name == b.Nick { | 	if event.Source.Name == b.Nick { | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
|  | 	// don't forward messages we sent via RELAYMSG | ||||||
|  | 	if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	// This is the old name of the cap sent in spoofed messages; I've kept this in | ||||||
|  | 	// for compatibility reasons | ||||||
|  | 	if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -302,3 +392,31 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) { | |||||||
| func (b *Birc) formatnicks(nicks []string) string { | func (b *Birc) formatnicks(nicks []string) string { | ||||||
| 	return strings.Join(nicks, ", ") + " currently on IRC" | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Birc) getMessageClipped() string { | ||||||
|  | 	if b.GetString("MessageClipped") == "" { | ||||||
|  | 		return " <clipped message>" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return " " + b.GetString("MessageClipped") | ||||||
|  | } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import ( | |||||||
| 	"strconv" | 	"strconv" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/keybase/go-keybase-chat-bot/kbchat" | 	"github.com/keybase/go-keybase-chat-bot/kbchat/types/chat1" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (b *Bkeybase) handleKeybase() { | func (b *Bkeybase) handleKeybase() { | ||||||
| @@ -20,7 +20,7 @@ func (b *Bkeybase) handleKeybase() { | |||||||
| 				b.Log.Errorf("failed to read message: %s", err.Error()) | 				b.Log.Errorf("failed to read message: %s", err.Error()) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if msg.Message.Content.Type != "text" { | 			if msg.Message.Content.TypeName != "text" { | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| @@ -34,7 +34,7 @@ func (b *Bkeybase) handleKeybase() { | |||||||
| 	}() | 	}() | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bkeybase) handleMessage(msg kbchat.Message) { | func (b *Bkeybase) handleMessage(msg chat1.MsgSummary) { | ||||||
| 	b.Log.Debugf("== Receiving event: %#v", msg) | 	b.Log.Debugf("== Receiving event: %#v", msg) | ||||||
| 	if msg.Channel.TopicName != b.channel || msg.Channel.Name != b.team { | 	if msg.Channel.TopicName != b.channel || msg.Channel.Name != b.team { | ||||||
| 		return | 		return | ||||||
| @@ -45,10 +45,10 @@ func (b *Bkeybase) handleMessage(msg kbchat.Message) { | |||||||
| 		// TODO download avatar | 		// TODO download avatar | ||||||
|  |  | ||||||
| 		// Create our message | 		// Create our message | ||||||
| 		rmsg := config.Message{Username: msg.Sender.Username, Text: msg.Content.Text.Body, UserID: msg.Sender.Uid, Channel: msg.Channel.TopicName, ID: strconv.Itoa(msg.MsgID), Account: b.Account} | 		rmsg := config.Message{Username: msg.Sender.Username, Text: msg.Content.Text.Body, UserID: string(msg.Sender.Uid), Channel: msg.Channel.TopicName, ID: strconv.Itoa(int(msg.Id)), Account: b.Account} | ||||||
|  |  | ||||||
| 		// Text must be a string | 		// Text must be a string | ||||||
| 		if msg.Content.Type != "text" { | 		if msg.Content.TypeName != "text" { | ||||||
| 			b.Log.Errorf("message is not text") | 			b.Log.Errorf("message is not text") | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -90,16 +90,17 @@ func (b *Bkeybase) Send(msg config.Message) (string, error) { | |||||||
| 				return "", err | 				return "", err | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			_, _ = b.kbc.SendAttachmentByTeam(b.team, fpath, fcaption, &b.channel) | 			_, _ = b.kbc.SendAttachmentByTeam(b.team, &b.channel, fpath, fcaption) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return "", nil | 		return "", nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Send regular message | 	// Send regular message | ||||||
| 	resp, err := b.kbc.SendMessageByTeamName(b.team, msg.Username+msg.Text, &b.channel) | 	text := msg.Username + msg.Text | ||||||
|  | 	resp, err := b.kbc.SendMessageByTeamName(b.team, &b.channel, text) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 	return strconv.Itoa(resp.Result.MsgID), err | 	return strconv.Itoa(int(*resp.Result.MessageID)), err | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										215
									
								
								bridge/matrix/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								bridge/matrix/helpers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | |||||||
|  | package bmatrix | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"html" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	matrix "github.com/matrix-org/gomatrix" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func newMatrixUsername(username string) *matrixUsername { | ||||||
|  | 	mUsername := new(matrixUsername) | ||||||
|  |  | ||||||
|  | 	// check if we have a </tag>. if we have, we don't escape HTML. #696 | ||||||
|  | 	if htmlTag.MatchString(username) { | ||||||
|  | 		mUsername.formatted = username | ||||||
|  | 		// remove the HTML formatting for beautiful push messages #1188 | ||||||
|  | 		mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "") | ||||||
|  | 	} else { | ||||||
|  | 		mUsername.formatted = html.EscapeString(username) | ||||||
|  | 		mUsername.plain = username | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return mUsername | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getRoomID retrieves a matching room ID from the channel name. | ||||||
|  | func (b *Bmatrix) getRoomID(channel string) string { | ||||||
|  | 	b.RLock() | ||||||
|  | 	defer b.RUnlock() | ||||||
|  | 	for ID, name := range b.RoomMap { | ||||||
|  | 		if name == channel { | ||||||
|  | 			return ID | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // interface2Struct marshals and immediately unmarshals an interface. | ||||||
|  | // Useful for converting map[string]interface{} to a struct. | ||||||
|  | func interface2Struct(in interface{}, out interface{}) error { | ||||||
|  | 	jsonObj, err := json.Marshal(in) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err //nolint:wrapcheck | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return json.Unmarshal(jsonObj, out) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getDisplayName retrieves the displayName for mxid, querying the homserver if the mxid is not in the cache. | ||||||
|  | func (b *Bmatrix) getDisplayName(mxid string) string { | ||||||
|  | 	if b.GetBool("UseUserName") { | ||||||
|  | 		return mxid[1:] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.RLock() | ||||||
|  | 	if val, present := b.NicknameMap[mxid]; present { | ||||||
|  | 		b.RUnlock() | ||||||
|  |  | ||||||
|  | 		return val.displayName | ||||||
|  | 	} | ||||||
|  | 	b.RUnlock() | ||||||
|  |  | ||||||
|  | 	displayName, err := b.mc.GetDisplayName(mxid) | ||||||
|  | 	var httpError *matrix.HTTPError | ||||||
|  | 	if errors.As(err, &httpError) { | ||||||
|  | 		b.Log.Warnf("Couldn't retrieve the display name for %s", mxid) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return b.cacheDisplayName(mxid, mxid[1:]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return b.cacheDisplayName(mxid, displayName.DisplayName) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // cacheDisplayName stores the mapping between a mxid and a display name, to be reused later without performing a query to the homserver. | ||||||
|  | // Note that old entries are cleaned when this function is called. | ||||||
|  | func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string { | ||||||
|  | 	now := time.Now() | ||||||
|  |  | ||||||
|  | 	// scan to delete old entries, to stop memory usage from becoming too high with old entries. | ||||||
|  | 	// In addition, we also detect if another user have the same username, and if so, we append their mxids to their usernames to differentiate them. | ||||||
|  | 	toDelete := []string{} | ||||||
|  | 	conflict := false | ||||||
|  |  | ||||||
|  | 	b.Lock() | ||||||
|  | 	for mxid, v := range b.NicknameMap { | ||||||
|  | 		// to prevent username reuse across matrix servers - or even on the same server, append | ||||||
|  | 		// the mxid to the username when there is a conflict | ||||||
|  | 		if v.displayName == displayName { | ||||||
|  | 			conflict = true | ||||||
|  | 			// TODO: it would be nice to be able to rename previous messages from this user. | ||||||
|  | 			// The current behavior is that only users with clashing usernames and *that have spoken since the bridge last started* will get their mxids shown, and I don't know if that's the expected behavior. | ||||||
|  | 			v.displayName = fmt.Sprintf("%s (%s)", displayName, mxid) | ||||||
|  | 			b.NicknameMap[mxid] = v | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if now.Sub(v.lastUpdated) > 10*time.Minute { | ||||||
|  | 			toDelete = append(toDelete, mxid) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if conflict { | ||||||
|  | 		displayName = fmt.Sprintf("%s (%s)", displayName, mxid) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, v := range toDelete { | ||||||
|  | 		delete(b.NicknameMap, v) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.NicknameMap[mxid] = NicknameCacheEntry{ | ||||||
|  | 		displayName: displayName, | ||||||
|  | 		lastUpdated: now, | ||||||
|  | 	} | ||||||
|  | 	b.Unlock() | ||||||
|  |  | ||||||
|  | 	return displayName | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleError converts errors into httpError. | ||||||
|  | //nolint:exhaustivestruct | ||||||
|  | func handleError(err error) *httpError { | ||||||
|  | 	var mErr matrix.HTTPError | ||||||
|  | 	if !errors.As(err, &mErr) { | ||||||
|  | 		return &httpError{ | ||||||
|  | 			Err: "not a HTTPError", | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var httpErr httpError | ||||||
|  |  | ||||||
|  | 	if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil { | ||||||
|  | 		return &httpError{ | ||||||
|  | 			Err: "unmarshal failed", | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &httpErr | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool { | ||||||
|  | 	// Skip empty messages | ||||||
|  | 	if content["msgtype"] == nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Only allow image,video or file msgtypes | ||||||
|  | 	if !(content["msgtype"].(string) == "m.image" || | ||||||
|  | 		content["msgtype"].(string) == "m.video" || | ||||||
|  | 		content["msgtype"].(string) == "m.file") { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getAvatarURL returns the avatar URL of the specified sender. | ||||||
|  | func (b *Bmatrix) getAvatarURL(sender string) string { | ||||||
|  | 	urlPath := b.mc.BuildURL("profile", sender, "avatar_url") | ||||||
|  |  | ||||||
|  | 	s := struct { | ||||||
|  | 		AvatarURL string `json:"avatar_url"` | ||||||
|  | 	}{} | ||||||
|  |  | ||||||
|  | 	err := b.mc.MakeRequest("GET", urlPath, nil, &s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("getAvatarURL failed: %s", err) | ||||||
|  |  | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/") | ||||||
|  | 	if url != "" { | ||||||
|  | 		url += "?width=37&height=37&method=crop" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return url | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleRatelimit handles the ratelimit errors and return if we're ratelimited and the amount of time to sleep | ||||||
|  | func (b *Bmatrix) handleRatelimit(err error) (time.Duration, bool) { | ||||||
|  | 	httpErr := handleError(err) | ||||||
|  | 	if httpErr.Errcode != "M_LIMIT_EXCEEDED" { | ||||||
|  | 		return 0, false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("ratelimited: %s", httpErr.Err) | ||||||
|  | 	b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before retrying", httpErr.RetryAfterMs/1000) | ||||||
|  |  | ||||||
|  | 	return time.Duration(httpErr.RetryAfterMs) * time.Millisecond, true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // retry function will check if we're ratelimited and retries again when backoff time expired | ||||||
|  | // returns original error if not 429 ratelimit | ||||||
|  | func (b *Bmatrix) retry(f func() error) error { | ||||||
|  | 	b.rateMutex.Lock() | ||||||
|  | 	defer b.rateMutex.Unlock() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		if err := f(); err != nil { | ||||||
|  | 			if backoff, ok := b.handleRatelimit(err); ok { | ||||||
|  | 				time.Sleep(backoff) | ||||||
|  | 			} else { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -3,52 +3,120 @@ package bmatrix | |||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html" |  | ||||||
| 	"mime" | 	"mime" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	matrix "github.com/matterbridge/gomatrix" | 	matrix "github.com/matrix-org/gomatrix" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	htmlTag            = regexp.MustCompile("</.*?>") | ||||||
|  | 	htmlReplacementTag = regexp.MustCompile("<[^>]*>") | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type NicknameCacheEntry struct { | ||||||
|  | 	displayName string | ||||||
|  | 	lastUpdated time.Time | ||||||
|  | } | ||||||
|  |  | ||||||
| type Bmatrix struct { | type Bmatrix struct { | ||||||
| 	mc      *matrix.Client | 	mc          *matrix.Client | ||||||
| 	UserID  string | 	UserID      string | ||||||
| 	RoomMap map[string]string | 	NicknameMap map[string]NicknameCacheEntry | ||||||
|  | 	RoomMap     map[string]string | ||||||
|  | 	rateMutex   sync.RWMutex | ||||||
| 	sync.RWMutex | 	sync.RWMutex | ||||||
| 	htmlTag *regexp.Regexp |  | ||||||
| 	*bridge.Config | 	*bridge.Config | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type httpError struct { | ||||||
|  | 	Errcode      string `json:"errcode"` | ||||||
|  | 	Err          string `json:"error"` | ||||||
|  | 	RetryAfterMs int    `json:"retry_after_ms"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type matrixUsername struct { | ||||||
|  | 	plain     string | ||||||
|  | 	formatted string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SubTextMessage represents the new content of the message in edit messages. | ||||||
|  | type SubTextMessage struct { | ||||||
|  | 	MsgType       string `json:"msgtype"` | ||||||
|  | 	Body          string `json:"body"` | ||||||
|  | 	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 { | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
| 	b := &Bmatrix{Config: cfg} | 	b := &Bmatrix{Config: cfg} | ||||||
| 	b.htmlTag = regexp.MustCompile("</.*?>") |  | ||||||
| 	b.RoomMap = make(map[string]string) | 	b.RoomMap = make(map[string]string) | ||||||
|  | 	b.NicknameMap = make(map[string]NicknameCacheEntry) | ||||||
| 	return b | 	return b | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bmatrix) Connect() error { | func (b *Bmatrix) Connect() error { | ||||||
| 	var err error | 	var err error | ||||||
| 	b.Log.Infof("Connecting %s", b.GetString("Server")) | 	b.Log.Infof("Connecting %s", b.GetString("Server")) | ||||||
| 	b.mc, err = matrix.NewClient(b.GetString("Server"), "", "") | 	if b.GetString("MxID") != "" && b.GetString("Token") != "" { | ||||||
| 	if err != nil { | 		b.mc, err = matrix.NewClient( | ||||||
| 		return err | 			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() | 	go b.handlematrix() | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @@ -58,14 +126,18 @@ func (b *Bmatrix) Disconnect() error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error { | func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error { | ||||||
| 	resp, err := b.mc.JoinRoom(channel.Name, "", nil) | 	return b.retry(func() error { | ||||||
| 	if err != nil { | 		resp, err := b.mc.JoinRoom(channel.Name, "", nil) | ||||||
| 		return err | 		if err != nil { | ||||||
| 	} | 			return err | ||||||
| 	b.Lock() | 		} | ||||||
| 	b.RoomMap[resp.RoomID] = channel.Name |  | ||||||
| 	b.Unlock() | 		b.Lock() | ||||||
| 	return err | 		b.RoomMap[resp.RoomID] = channel.Name | ||||||
|  | 		b.Unlock() | ||||||
|  |  | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bmatrix) Send(msg config.Message) (string, error) { | func (b *Bmatrix) Send(msg config.Message) (string, error) { | ||||||
| @@ -74,17 +146,36 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | |||||||
| 	channel := b.getRoomID(msg.Channel) | 	channel := b.getRoomID(msg.Channel) | ||||||
| 	b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel) | 	b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel) | ||||||
|  |  | ||||||
|  | 	username := newMatrixUsername(msg.Username) | ||||||
|  |  | ||||||
| 	// Make a action /me of the message | 	// Make a action /me of the message | ||||||
| 	if msg.Event == config.EventUserAction { | 	if msg.Event == config.EventUserAction { | ||||||
| 		m := matrix.TextMessage{ | 		m := matrix.TextMessage{ | ||||||
| 			MsgType: "m.emote", | 			MsgType:       "m.emote", | ||||||
| 			Body:    msg.Username + msg.Text, | 			Body:          username.plain + msg.Text, | ||||||
|  | 			FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text), | ||||||
|  | 			Format:        "org.matrix.custom.html", | ||||||
| 		} | 		} | ||||||
| 		resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m) |  | ||||||
| 		if err != nil { | 		if b.GetBool("HTMLDisable") { | ||||||
| 			return "", err | 			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 | 	// Delete message | ||||||
| @@ -92,17 +183,34 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | |||||||
| 		if msg.ID == "" { | 		if msg.ID == "" { | ||||||
| 			return "", nil | 			return "", nil | ||||||
| 		} | 		} | ||||||
| 		resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{}) |  | ||||||
| 		if err != nil { | 		msgID := "" | ||||||
| 			return "", err |  | ||||||
| 		} | 		err := b.retry(func() error { | ||||||
| 		return resp.EventID, err | 			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 | 	// Upload a file if it exists | ||||||
| 	if msg.Extra != nil { | 	if msg.Extra != nil { | ||||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | ||||||
| 			if _, 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) | 				b.Log.Errorf("sendText failed: %s", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -113,47 +221,163 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Edit message if we have an ID | 	// Edit message if we have an ID | ||||||
| 	// matrix has no editing support | 	if msg.ID != "" { | ||||||
|  | 		rmsg := EditedMessage{ | ||||||
|  | 			TextMessage: matrix.TextMessage{ | ||||||
|  | 				Body:          username.plain + msg.Text, | ||||||
|  | 				MsgType:       "m.text", | ||||||
|  | 				Format:        "org.matrix.custom.html", | ||||||
|  | 				FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text), | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 	// Use notices to send join/leave events | 		rmsg.NewContent = SubTextMessage{ | ||||||
| 	if msg.Event == config.EventJoinLeave { | 			Body:          rmsg.TextMessage.Body, | ||||||
| 		resp, err := b.mc.SendNotice(channel, msg.Username+msg.Text) | 			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 { | 		if err != nil { | ||||||
| 			return "", err | 			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:          username.plain + msg.Text, | ||||||
|  | 			FormattedBody: username.formatted + msg.Text, | ||||||
|  | 			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 | 		return resp.EventID, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	username := html.EscapeString(msg.Username) | 	if msg.ParentValid() { | ||||||
| 	// check if we have a </tag>. if we have, we don't escape HTML. #696 | 		m := ReplyMessage{ | ||||||
| 	if b.htmlTag.MatchString(msg.Username) { | 			TextMessage: matrix.TextMessage{ | ||||||
| 		username = msg.Username | 				MsgType:       "m.text", | ||||||
|  | 				Body:          username.plain + msg.Text, | ||||||
|  | 				FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text), | ||||||
|  | 				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, username.plain+msg.Text) | ||||||
|  |  | ||||||
|  | 			return err | ||||||
|  | 		}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return resp.EventID, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Post normal message with HTML support (eg riot.im) | 	// Post normal message with HTML support (eg riot.im) | ||||||
| 	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, username.plain+msg.Text, | ||||||
|  | 			username.formatted+helper.ParseMarkdown(msg.Text)) | ||||||
|  |  | ||||||
|  | 		return err | ||||||
|  | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 	return resp.EventID, err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (b *Bmatrix) getRoomID(channel string) string { | 	return resp.EventID, err | ||||||
| 	b.RLock() |  | ||||||
| 	defer b.RUnlock() |  | ||||||
| 	for ID, name := range b.RoomMap { |  | ||||||
| 		if name == channel { |  | ||||||
| 			return ID |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return "" |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bmatrix) handlematrix() { | func (b *Bmatrix) handlematrix() { | ||||||
| 	syncer := b.mc.Syncer.(*matrix.DefaultSyncer) | 	syncer := b.mc.Syncer.(*matrix.DefaultSyncer) | ||||||
| 	syncer.OnEventType("m.room.redaction", b.handleEvent) | 	syncer.OnEventType("m.room.redaction", b.handleEvent) | ||||||
| 	syncer.OnEventType("m.room.message", b.handleEvent) | 	syncer.OnEventType("m.room.message", b.handleEvent) | ||||||
|  | 	syncer.OnEventType("m.room.member", b.handleMemberChange) | ||||||
| 	go func() { | 	go func() { | ||||||
| 		for { | 		for { | ||||||
|  | 			if b == nil { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
| 			if err := b.mc.Sync(); err != nil { | 			if err := b.mc.Sync(); err != nil { | ||||||
| 				b.Log.Println("Sync() returned ", err) | 				b.Log.Println("Sync() returned ", err) | ||||||
| 			} | 			} | ||||||
| @@ -161,6 +385,74 @@ func (b *Bmatrix) handlematrix() { | |||||||
| 	}() | 	}() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool { | ||||||
|  | 	relationInterface, present := ev.Content["m.relates_to"] | ||||||
|  | 	newContentInterface, present2 := ev.Content["m.new_content"] | ||||||
|  | 	if !(present && present2) { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var relation MessageRelation | ||||||
|  | 	if err := interface2Struct(relationInterface, &relation); err != nil { | ||||||
|  | 		b.Log.Warnf("Couldn't parse 'm.relates_to' object with value %#v", relationInterface) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var newContent SubTextMessage | ||||||
|  | 	if err := interface2Struct(newContentInterface, &newContent); err != nil { | ||||||
|  | 		b.Log.Warnf("Couldn't parse 'm.new_content' object with value %#v", newContentInterface) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if relation.Type != "m.replace" { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rmsg.ID = relation.EventID | ||||||
|  | 	rmsg.Text = newContent.Body | ||||||
|  | 	b.Remote <- rmsg | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool { | ||||||
|  | 	relationInterface, present := ev.Content["m.relates_to"] | ||||||
|  | 	if !present { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var relation InReplyToRelation | ||||||
|  | 	if err := interface2Struct(relationInterface, &relation); err != nil { | ||||||
|  | 		// probably fine | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	body := rmsg.Text | ||||||
|  | 	for strings.HasPrefix(body, "> ") { | ||||||
|  | 		lineIdx := strings.IndexRune(body, '\n') | ||||||
|  | 		if lineIdx == -1 { | ||||||
|  | 			body = "" | ||||||
|  | 		} else { | ||||||
|  | 			body = body[(lineIdx + 1):] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rmsg.Text = body | ||||||
|  | 	rmsg.ParentID = relation.InReplyTo.EventID | ||||||
|  | 	b.Remote <- rmsg | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmatrix) handleMemberChange(ev *matrix.Event) { | ||||||
|  | 	// Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information | ||||||
|  | 	if ev.Content["membership"] == "join" { | ||||||
|  | 		if dn, ok := ev.Content["displayname"].(string); ok { | ||||||
|  | 			b.cacheDisplayName(ev.Sender, dn) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func (b *Bmatrix) handleEvent(ev *matrix.Event) { | func (b *Bmatrix) handleEvent(ev *matrix.Event) { | ||||||
| 	b.Log.Debugf("== Receiving event: %#v", ev) | 	b.Log.Debugf("== Receiving event: %#v", ev) | ||||||
| 	if ev.Sender != b.UserID { | 	if ev.Sender != b.UserID { | ||||||
| @@ -172,16 +464,14 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// TODO download avatar |  | ||||||
|  |  | ||||||
| 		// Create our message | 		// Create our message | ||||||
| 		rmsg := config.Message{Username: ev.Sender[1:], Channel: channel, Account: b.Account, UserID: ev.Sender, ID: ev.ID} | 		rmsg := config.Message{ | ||||||
|  | 			Username: b.getDisplayName(ev.Sender), | ||||||
| 		// Text must be a string | 			Channel:  channel, | ||||||
| 		if rmsg.Text, ok = ev.Content["body"].(string); !ok { | 			Account:  b.Account, | ||||||
| 			b.Log.Errorf("Content[body] is not a string: %T\n%#v", | 			UserID:   ev.Sender, | ||||||
| 				ev.Content["body"], ev.Content) | 			ID:       ev.ID, | ||||||
| 			return | 			Avatar:   b.getAvatarURL(ev.Sender), | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Remove homeserver suffix if configured | 		// Remove homeserver suffix if configured | ||||||
| @@ -199,11 +489,28 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// Text must be a string | ||||||
|  | 		if rmsg.Text, ok = ev.Content["body"].(string); !ok { | ||||||
|  | 			b.Log.Errorf("Content[body] is not a string: %T\n%#v", | ||||||
|  | 				ev.Content["body"], ev.Content) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// Do we have a /me action | 		// Do we have a /me action | ||||||
| 		if ev.Content["msgtype"].(string) == "m.emote" { | 		if ev.Content["msgtype"].(string) == "m.emote" { | ||||||
| 			rmsg.Event = config.EventUserAction | 			rmsg.Event = config.EventUserAction | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// Is it an edit? | ||||||
|  | 		if b.handleEdit(ev, rmsg) { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Is it a reply? | ||||||
|  | 		if b.handleReply(ev, rmsg) { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// Do we have attachments | 		// Do we have attachments | ||||||
| 		if b.containsAttachment(ev.Content) { | 		if b.containsAttachment(ev.Content) { | ||||||
| 			err := b.handleDownloadFile(&rmsg, ev.Content) | 			err := b.handleDownloadFile(&rmsg, ev.Content) | ||||||
| @@ -214,6 +521,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { | |||||||
|  |  | ||||||
| 		b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account) | 		b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account) | ||||||
| 		b.Remote <- rmsg | 		b.Remote <- rmsg | ||||||
|  |  | ||||||
|  | 		// not crucial, so no ratelimit check here | ||||||
|  | 		if err := b.mc.MarkRead(ev.RoomID, ev.ID); err != nil { | ||||||
|  | 			b.Log.Errorf("couldn't mark message as read %s", err.Error()) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -288,27 +600,30 @@ func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string | |||||||
|  |  | ||||||
| // handleUploadFile handles native upload of a file. | // handleUploadFile handles native upload of a file. | ||||||
| func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) { | func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) { | ||||||
|  | 	username := newMatrixUsername(msg.Username) | ||||||
| 	content := bytes.NewReader(*fi.Data) | 	content := bytes.NewReader(*fi.Data) | ||||||
| 	sp := strings.Split(fi.Name, ".") | 	sp := strings.Split(fi.Name, ".") | ||||||
| 	mtype := mime.TypeByExtension("." + sp[len(sp)-1]) | 	mtype := mime.TypeByExtension("." + sp[len(sp)-1]) | ||||||
| 	if !(strings.Contains(mtype, "image") || strings.Contains(mtype, "video") || | 	// image and video uploads send no username, we have to do this ourself here #715 | ||||||
| 		strings.Contains(mtype, "application") || strings.Contains(mtype, "audio")) { | 	err := b.retry(func() error { | ||||||
| 		return | 		_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment) | ||||||
| 	} |  | ||||||
| 	if fi.Comment != "" { | 		return err | ||||||
| 		_, err := b.mc.SendText(channel, msg.Username+fi.Comment) | 	}) | ||||||
| 		if err != nil { | 	if err != nil { | ||||||
| 			b.Log.Errorf("file comment failed: %#v", err) | 		b.Log.Errorf("file comment failed: %#v", err) | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		// image and video uploads send no username, we have to do this ourself here #715 |  | ||||||
| 		_, err := b.mc.SendText(channel, msg.Username) |  | ||||||
| 		if err != nil { |  | ||||||
| 			b.Log.Errorf("file comment failed: %#v", err) |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.Log.Debugf("uploading file: %s %s", fi.Name, mtype) | 	b.Log.Debugf("uploading file: %s %s", fi.Name, mtype) | ||||||
| 	res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data))) |  | ||||||
|  | 	var res *matrix.RespMediaUpload | ||||||
|  |  | ||||||
|  | 	err = b.retry(func() error { | ||||||
|  | 		res, err = b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data))) | ||||||
|  |  | ||||||
|  | 		return err | ||||||
|  | 	}) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		b.Log.Errorf("file upload failed: %#v", err) | 		b.Log.Errorf("file upload failed: %#v", err) | ||||||
| 		return | 		return | ||||||
| @@ -317,44 +632,60 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf | |||||||
| 	switch { | 	switch { | ||||||
| 	case strings.Contains(mtype, "video"): | 	case strings.Contains(mtype, "video"): | ||||||
| 		b.Log.Debugf("sendVideo %s", res.ContentURI) | 		b.Log.Debugf("sendVideo %s", res.ContentURI) | ||||||
| 		_, err = b.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 { | 		if err != nil { | ||||||
| 			b.Log.Errorf("sendVideo failed: %#v", err) | 			b.Log.Errorf("sendVideo failed: %#v", err) | ||||||
| 		} | 		} | ||||||
| 	case strings.Contains(mtype, "image"): | 	case strings.Contains(mtype, "image"): | ||||||
| 		b.Log.Debugf("sendImage %s", res.ContentURI) | 		b.Log.Debugf("sendImage %s", res.ContentURI) | ||||||
| 		_, err = b.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 { | 		if err != nil { | ||||||
| 			b.Log.Errorf("sendImage failed: %#v", err) | 			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"): | 	case strings.Contains(mtype, "audio"): | ||||||
| 		b.Log.Debugf("sendAudio %s", res.ContentURI) | 		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 { | 		if err != nil { | ||||||
| 			b.Log.Errorf("sendAudio failed: %#v", err) | 			b.Log.Errorf("sendAudio failed: %#v", err) | ||||||
| 		} | 		} | ||||||
|  | 	default: | ||||||
|  | 		b.Log.Debugf("sendFile %s", res.ContentURI) | ||||||
|  | 		err = b.retry(func() error { | ||||||
|  | 			_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{ | ||||||
|  | 				MsgType: "m.file", | ||||||
|  | 				Body:    fi.Name, | ||||||
|  | 				URL:     res.ContentURI, | ||||||
|  | 				Info: matrix.FileInfo{ | ||||||
|  | 					Mimetype: mtype, | ||||||
|  | 					Size:     uint(len(*fi.Data)), | ||||||
|  | 				}, | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			return err | ||||||
|  | 		}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			b.Log.Errorf("sendFile failed: %#v", err) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	b.Log.Debugf("result: %#v", res) | 	b.Log.Debugf("result: %#v", res) | ||||||
| } | } | ||||||
|  |  | ||||||
| // skipMessages returns true if this message should not be handled |  | ||||||
| func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool { |  | ||||||
| 	// Skip empty messages |  | ||||||
| 	if content["msgtype"] == nil { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Only allow image,video or file msgtypes |  | ||||||
| 	if !(content["msgtype"].(string) == "m.image" || |  | ||||||
| 		content["msgtype"].(string) == "m.video" || |  | ||||||
| 		content["msgtype"].(string) == "m.file") { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	return true |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								bridge/matrix/matrix_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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) | ||||||
|  | } | ||||||
| @@ -4,7 +4,9 @@ import ( | |||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/42wim/matterbridge/matterclient" | 	"github.com/42wim/matterbridge/matterclient" | ||||||
| 	"github.com/mattermost/mattermost-server/model" | 	matterclient6 "github.com/matterbridge/matterclient" | ||||||
|  | 	"github.com/mattermost/mattermost-server/v5/model" | ||||||
|  | 	model6 "github.com/mattermost/mattermost-server/v6/model" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // handleDownloadAvatar downloads the avatar of userid from channel | // handleDownloadAvatar downloads the avatar of userid from channel | ||||||
| @@ -21,12 +23,26 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) { | |||||||
| 		Extra:    make(map[string][]interface{}), | 		Extra:    make(map[string][]interface{}), | ||||||
| 	} | 	} | ||||||
| 	if _, ok := b.avatarMap[userid]; !ok { | 	if _, ok := b.avatarMap[userid]; !ok { | ||||||
| 		data, resp := b.mc.Client.GetProfileImage(userid, "") | 		var ( | ||||||
| 		if resp.Error != nil { | 			data []byte | ||||||
| 			b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error) | 			err  error | ||||||
| 			return | 			resp *model.Response | ||||||
|  | 		) | ||||||
|  | 		if b.mc6 != nil { | ||||||
|  | 			data, _, err = b.mc6.Client.GetProfileImage(userid, "") | ||||||
|  | 			if err != nil { | ||||||
|  | 				b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			data, resp = b.mc.Client.GetProfileImage(userid, "") | ||||||
|  | 			if resp.Error != nil { | ||||||
|  | 				b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 		err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General) |  | ||||||
|  | 		err = helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			b.Log.Error(err) | 			b.Log.Error(err) | ||||||
| 			return | 			return | ||||||
| @@ -38,6 +54,10 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) { | |||||||
|  |  | ||||||
| // handleDownloadFile handles file download | // handleDownloadFile handles file download | ||||||
| func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error { | func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error { | ||||||
|  | 	if b.mc6 != nil { | ||||||
|  | 		return b.handleDownloadFile6(rmsg, id) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	url, _ := b.mc.Client.GetFileLink(id) | 	url, _ := b.mc.Client.GetFileLink(id) | ||||||
| 	finfo, resp := b.mc.Client.GetFileInfo(id) | 	finfo, resp := b.mc.Client.GetFileInfo(id) | ||||||
| 	if resp.Error != nil { | 	if resp.Error != nil { | ||||||
| @@ -55,6 +75,25 @@ func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // nolint:wrapcheck | ||||||
|  | func (b *Bmattermost) handleDownloadFile6(rmsg *config.Message, id string) error { | ||||||
|  | 	url, _, _ := b.mc6.Client.GetFileLink(id) | ||||||
|  | 	finfo, _, err := b.mc6.Client.GetFileInfo(id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	err = helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	data, _, err := b.mc6.Client.DownloadFile(id, true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (b *Bmattermost) handleMatter() { | func (b *Bmattermost) handleMatter() { | ||||||
| 	messages := make(chan *config.Message) | 	messages := make(chan *config.Message) | ||||||
| 	if b.GetString("WebhookBindAddress") != "" { | 	if b.GetString("WebhookBindAddress") != "" { | ||||||
| @@ -67,9 +106,8 @@ func (b *Bmattermost) handleMatter() { | |||||||
| 			b.Log.Debugf("Choosing login/password based receiving") | 			b.Log.Debugf("Choosing login/password based receiving") | ||||||
| 		} | 		} | ||||||
| 		// if for some reason we only want to sent stuff to mattermost but not receive, return | 		// if for some reason we only want to sent stuff to mattermost but not receive, return | ||||||
| 		if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") != "" { | 		if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") != "" && b.GetString("Token") == "" && b.GetString("Login") == "" { | ||||||
| 			b.Log.Debugf("No WebhookBindAddress specified, only WebhookURL. You will not receive messages from mattermost, only sending is possible.") | 			b.Log.Debugf("No WebhookBindAddress specified, only WebhookURL. You will not receive messages from mattermost, only sending is possible.") | ||||||
| 			return |  | ||||||
| 		} | 		} | ||||||
| 		go b.handleMatterClient(messages) | 		go b.handleMatterClient(messages) | ||||||
| 	} | 	} | ||||||
| @@ -88,6 +126,12 @@ func (b *Bmattermost) handleMatter() { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { | func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { | ||||||
|  | 	if b.mc6 != nil { | ||||||
|  | 		b.Log.Debug("starting matterclient6") | ||||||
|  | 		b.handleMatterClient6(messages) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	for message := range b.mc.MessageChan { | 	for message := range b.mc.MessageChan { | ||||||
| 		b.Log.Debugf("%#v", message.Raw.Data) | 		b.Log.Debugf("%#v", message.Raw.Data) | ||||||
|  |  | ||||||
| @@ -96,9 +140,14 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { | |||||||
| 			continue | 			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) | 		// only download avatars if we have a place to upload them (configured mediaserver) | ||||||
| 		if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" { | 		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) | 		b.Log.Debugf("== Receiving event %#v", message) | ||||||
| @@ -106,10 +155,10 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { | |||||||
| 		rmsg := &config.Message{ | 		rmsg := &config.Message{ | ||||||
| 			Username: message.Username, | 			Username: message.Username, | ||||||
| 			UserID:   message.UserID, | 			UserID:   message.UserID, | ||||||
| 			Channel:  message.Channel, | 			Channel:  channelName, | ||||||
| 			Text:     message.Text, | 			Text:     message.Text, | ||||||
| 			ID:       message.Post.Id, | 			ID:       message.Post.Id, | ||||||
| 			ParentID: message.Post.ParentId, | 			ParentID: message.Post.RootId, // ParentID is obsolete with mattermost | ||||||
| 			Extra:    make(map[string][]interface{}), | 			Extra:    make(map[string][]interface{}), | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -133,8 +182,72 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Use nickname instead of username if defined | 		// Use nickname instead of username if defined | ||||||
| 		if nick := b.mc.GetNickName(rmsg.UserID); nick != "" { | 		if !b.GetBool("useusername") { | ||||||
| 			rmsg.Username = nick | 			if nick := b.mc.GetNickName(rmsg.UserID); nick != "" { | ||||||
|  | 				rmsg.Username = nick | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		messages <- rmsg | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // nolint:cyclop | ||||||
|  | func (b *Bmattermost) handleMatterClient6(messages chan *config.Message) { | ||||||
|  | 	for message := range b.mc6.MessageChan { | ||||||
|  | 		b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType()) | ||||||
|  |  | ||||||
|  | 		if b.skipMessage6(message) { | ||||||
|  | 			b.Log.Debugf("Skipped message: %#v", message) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		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, channelName) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		b.Log.Debugf("== Receiving event %#v", message) | ||||||
|  |  | ||||||
|  | 		rmsg := &config.Message{ | ||||||
|  | 			Username: message.Username, | ||||||
|  | 			UserID:   message.UserID, | ||||||
|  | 			Channel:  channelName, | ||||||
|  | 			Text:     message.Text, | ||||||
|  | 			ID:       message.Post.Id, | ||||||
|  | 			ParentID: message.Post.RootId, // ParentID is obsolete with mattermost | ||||||
|  | 			Extra:    make(map[string][]interface{}), | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// handle mattermost post properties (override username and attachments) | ||||||
|  | 		b.handleProps6(rmsg, message) | ||||||
|  |  | ||||||
|  | 		// create a text for bridges that don't support native editing | ||||||
|  | 		if message.Raw.EventType() == model6.WebsocketEventPostEdited && !b.GetBool("EditDisable") { | ||||||
|  | 			rmsg.Text = message.Text + b.GetString("EditSuffix") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if message.Raw.EventType() == model6.WebsocketEventPostDeleted { | ||||||
|  | 			rmsg.Event = config.EventMsgDelete | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, id := range message.Post.FileIds { | ||||||
|  | 			err := b.handleDownloadFile(rmsg, id) | ||||||
|  | 			if err != nil { | ||||||
|  | 				b.Log.Errorf("download failed: %s", err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Use nickname instead of username if defined | ||||||
|  | 		if !b.GetBool("useusername") { | ||||||
|  | 			if nick := b.mc6.GetNickName(rmsg.UserID); nick != "" { | ||||||
|  | 				rmsg.Username = nick | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		messages <- rmsg | 		messages <- rmsg | ||||||
| @@ -145,6 +258,7 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) { | |||||||
| 	for { | 	for { | ||||||
| 		message := b.mh.Receive() | 		message := b.mh.Receive() | ||||||
| 		b.Log.Debugf("Receiving from matterhook %#v", message) | 		b.Log.Debugf("Receiving from matterhook %#v", message) | ||||||
|  |  | ||||||
| 		messages <- &config.Message{ | 		messages <- &config.Message{ | ||||||
| 			UserID:   message.UserID, | 			UserID:   message.UserID, | ||||||
| 			Username: message.UserName, | 			Username: message.UserName, | ||||||
| @@ -156,9 +270,13 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) { | |||||||
|  |  | ||||||
| // handleUploadFile handles native upload of files | // handleUploadFile handles native upload of files | ||||||
| func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) { | func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) { | ||||||
|  | 	if b.mc6 != nil { | ||||||
|  | 		return b.handleUploadFile6(msg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	var err error | 	var err error | ||||||
| 	var res, id string | 	var res, id string | ||||||
| 	channelID := b.mc.GetChannelId(msg.Channel, b.TeamID) | 	channelID := b.getChannelID(msg.Channel) | ||||||
| 	for _, f := range msg.Extra["file"] { | 	for _, f := range msg.Extra["file"] { | ||||||
| 		fi := f.(config.FileInfo) | 		fi := f.(config.FileInfo) | ||||||
| 		id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name) | 		id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name) | ||||||
| @@ -174,6 +292,26 @@ func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) { | |||||||
| 	return res, err | 	return res, err | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // nolint:forcetypeassert,wrapcheck | ||||||
|  | func (b *Bmattermost) handleUploadFile6(msg *config.Message) (string, error) { | ||||||
|  | 	var err error | ||||||
|  | 	var res, id string | ||||||
|  | 	channelID := b.getChannelID(msg.Channel) | ||||||
|  | 	for _, f := range msg.Extra["file"] { | ||||||
|  | 		fi := f.(config.FileInfo) | ||||||
|  | 		id, err = b.mc6.UploadFile(*fi.Data, channelID, fi.Name) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  | 		msg.Text = fi.Comment | ||||||
|  | 		if b.GetBool("PrefixMessagesWithNick") { | ||||||
|  | 			msg.Text = msg.Username + msg.Text | ||||||
|  | 		} | ||||||
|  | 		res, err = b.mc6.PostMessageWithFiles(channelID, msg.Text, msg.ParentID, []string{id}) | ||||||
|  | 	} | ||||||
|  | 	return res, err | ||||||
|  | } | ||||||
|  |  | ||||||
| func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) { | func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) { | ||||||
| 	props := message.Post.Props | 	props := message.Post.Props | ||||||
| 	if props == nil { | 	if props == nil { | ||||||
| @@ -198,3 +336,31 @@ func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Me | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // nolint:forcetypeassert | ||||||
|  | func (b *Bmattermost) handleProps6(rmsg *config.Message, message *matterclient6.Message) { | ||||||
|  | 	props := message.Post.Props | ||||||
|  | 	if props == nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if _, ok := props["override_username"].(string); ok { | ||||||
|  | 		rmsg.Username = props["override_username"].(string) | ||||||
|  | 	} | ||||||
|  | 	if _, ok := props["attachments"].([]interface{}); ok { | ||||||
|  | 		rmsg.Extra["attachments"] = props["attachments"].([]interface{}) | ||||||
|  | 		if rmsg.Text != "" { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, attachment := range rmsg.Extra["attachments"] { | ||||||
|  | 			attach := attachment.(map[string]interface{}) | ||||||
|  | 			if attach["text"].(string) != "" { | ||||||
|  | 				rmsg.Text += attach["text"].(string) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			if attach["fallback"].(string) != "" { | ||||||
|  | 				rmsg.Text += attach["fallback"].(string) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,13 +1,16 @@ | |||||||
| package bmattermost | package bmattermost | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/42wim/matterbridge/matterclient" | 	"github.com/42wim/matterbridge/matterclient" | ||||||
| 	"github.com/42wim/matterbridge/matterhook" | 	"github.com/42wim/matterbridge/matterhook" | ||||||
| 	"github.com/mattermost/mattermost-server/model" | 	matterclient6 "github.com/matterbridge/matterclient" | ||||||
|  | 	"github.com/mattermost/mattermost-server/v5/model" | ||||||
|  | 	model6 "github.com/mattermost/mattermost-server/v6/model" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (b *Bmattermost) doConnectWebhookBind() error { | func (b *Bmattermost) doConnectWebhookBind() error { | ||||||
| @@ -15,25 +18,47 @@ func (b *Bmattermost) doConnectWebhookBind() error { | |||||||
| 	case b.GetString("WebhookURL") != "": | 	case b.GetString("WebhookURL") != "": | ||||||
| 		b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") | 		b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") | ||||||
| 		b.mh = matterhook.New(b.GetString("WebhookURL"), | 		b.mh = matterhook.New(b.GetString("WebhookURL"), | ||||||
| 			matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | 			matterhook.Config{ | ||||||
| 				BindAddress: b.GetString("WebhookBindAddress")}) | 				InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | ||||||
|  | 				BindAddress:        b.GetString("WebhookBindAddress"), | ||||||
|  | 			}) | ||||||
| 	case b.GetString("Token") != "": | 	case b.GetString("Token") != "": | ||||||
| 		b.Log.Info("Connecting using token (sending)") | 		b.Log.Info("Connecting using token (sending)") | ||||||
| 		err := b.apiLogin() | 		b.Log.Infof("Using mattermost v6 methods: %t", b.v6) | ||||||
| 		if err != nil { |  | ||||||
| 			return err | 		if b.v6 { | ||||||
|  | 			err := b.apiLogin6() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			err := b.apiLogin() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	case b.GetString("Login") != "": | 	case b.GetString("Login") != "": | ||||||
| 		b.Log.Info("Connecting using login/password (sending)") | 		b.Log.Info("Connecting using login/password (sending)") | ||||||
| 		err := b.apiLogin() | 		b.Log.Infof("Using mattermost v6 methods: %t", b.v6) | ||||||
| 		if err != nil { |  | ||||||
| 			return err | 		if b.v6 { | ||||||
|  | 			err := b.apiLogin6() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			err := b.apiLogin() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	default: | 	default: | ||||||
| 		b.Log.Info("Connecting using webhookbindaddress (receiving)") | 		b.Log.Info("Connecting using webhookbindaddress (receiving)") | ||||||
| 		b.mh = matterhook.New(b.GetString("WebhookURL"), | 		b.mh = matterhook.New(b.GetString("WebhookURL"), | ||||||
| 			matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | 			matterhook.Config{ | ||||||
| 				BindAddress: b.GetString("WebhookBindAddress")}) | 				InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | ||||||
|  | 				BindAddress:        b.GetString("WebhookBindAddress"), | ||||||
|  | 			}) | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @@ -41,19 +66,39 @@ func (b *Bmattermost) doConnectWebhookBind() error { | |||||||
| func (b *Bmattermost) doConnectWebhookURL() error { | func (b *Bmattermost) doConnectWebhookURL() error { | ||||||
| 	b.Log.Info("Connecting using webhookurl (sending)") | 	b.Log.Info("Connecting using webhookurl (sending)") | ||||||
| 	b.mh = matterhook.New(b.GetString("WebhookURL"), | 	b.mh = matterhook.New(b.GetString("WebhookURL"), | ||||||
| 		matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | 		matterhook.Config{ | ||||||
| 			DisableServer: true}) | 			InsecureSkipVerify: b.GetBool("SkipTLSVerify"), | ||||||
|  | 			DisableServer:      true, | ||||||
|  | 		}) | ||||||
| 	if b.GetString("Token") != "" { | 	if b.GetString("Token") != "" { | ||||||
| 		b.Log.Info("Connecting using token (receiving)") | 		b.Log.Info("Connecting using token (receiving)") | ||||||
| 		err := b.apiLogin() | 		b.Log.Infof("Using mattermost v6 methods: %t", b.v6) | ||||||
| 		if err != nil { |  | ||||||
| 			return err | 		if b.v6 { | ||||||
|  | 			err := b.apiLogin6() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			err := b.apiLogin() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} else if b.GetString("Login") != "" { | 	} else if b.GetString("Login") != "" { | ||||||
| 		b.Log.Info("Connecting using login/password (receiving)") | 		b.Log.Info("Connecting using login/password (receiving)") | ||||||
| 		err := b.apiLogin() | 		b.Log.Infof("Using mattermost v6 methods: %t", b.v6) | ||||||
| 		if err != nil { |  | ||||||
| 			return err | 		if b.v6 { | ||||||
|  | 			err := b.apiLogin6() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			err := b.apiLogin() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| @@ -84,6 +129,31 @@ func (b *Bmattermost) apiLogin() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // nolint:wrapcheck | ||||||
|  | func (b *Bmattermost) apiLogin6() error { | ||||||
|  | 	password := b.GetString("Password") | ||||||
|  | 	if b.GetString("Token") != "" { | ||||||
|  | 		password = "token=" + b.GetString("Token") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.mc6 = matterclient6.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"), "") | ||||||
|  | 	if b.GetBool("debug") { | ||||||
|  | 		b.mc6.SetLogLevel("debug") | ||||||
|  | 	} | ||||||
|  | 	b.mc6.SkipTLSVerify = b.GetBool("SkipTLSVerify") | ||||||
|  | 	b.mc6.SkipVersionCheck = b.GetBool("SkipVersionCheck") | ||||||
|  | 	b.mc6.NoTLS = b.GetBool("NoTLS") | ||||||
|  | 	b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server")) | ||||||
|  |  | ||||||
|  | 	if err := b.mc6.Login(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.Log.Info("Connection succeeded") | ||||||
|  | 	b.TeamID = b.mc6.GetTeamID() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // replaceAction replace the message with the correct action (/me) code | // replaceAction replace the message with the correct action (/me) code | ||||||
| func (b *Bmattermost) replaceAction(text string) (string, bool) { | func (b *Bmattermost) replaceAction(text string) (string, bool) { | ||||||
| 	if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") { | 	if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") { | ||||||
| @@ -171,11 +241,17 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool { | |||||||
| 		if b.GetBool("nosendjoinpart") { | 		if b.GetBool("nosendjoinpart") { | ||||||
| 			return true | 			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.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account) | ||||||
| 		b.Remote <- config.Message{ | 		b.Remote <- config.Message{ | ||||||
| 			Username: "system", | 			Username: "system", | ||||||
| 			Text:     message.Text, | 			Text:     message.Text, | ||||||
| 			Channel:  message.Channel, | 			Channel:  channelName, | ||||||
| 			Account:  b.Account, | 			Account:  b.Account, | ||||||
| 			Event:    config.EventJoinLeave, | 			Event:    config.EventJoinLeave, | ||||||
| 		} | 		} | ||||||
| @@ -223,3 +299,119 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool { | |||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // skipMessages returns true if this message should not be handled | ||||||
|  | // nolint:gocyclo,cyclop | ||||||
|  | func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool { | ||||||
|  | 	// Handle join/leave | ||||||
|  | 	if message.Type == "system_join_leave" || | ||||||
|  | 		message.Type == "system_join_channel" || | ||||||
|  | 		message.Type == "system_leave_channel" { | ||||||
|  | 		if b.GetBool("nosendjoinpart") { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		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:  channelName, | ||||||
|  | 			Account:  b.Account, | ||||||
|  | 			Event:    config.EventJoinLeave, | ||||||
|  | 		} | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Handle edited messages | ||||||
|  | 	if (message.Raw.EventType() == model6.WebsocketEventPostEdited) && b.GetBool("EditDisable") { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Ignore non-post messages | ||||||
|  | 	if message.Post == nil { | ||||||
|  | 		b.Log.Debugf("ignoring nil message.Post: %#v", message) | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Ignore messages sent from matterbridge | ||||||
|  | 	if message.Post.Props != nil { | ||||||
|  | 		if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok { | ||||||
|  | 			b.Log.Debug("sent by matterbridge, ignoring") | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Ignore messages sent from a user logged in as the bot | ||||||
|  | 	if b.mc6.User.Username == message.Username { | ||||||
|  | 		b.Log.Debug("message from same user as bot, ignoring") | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// if the message has reactions don't repost it (for now, until we can correlate reaction with message) | ||||||
|  | 	if message.Post.HasReactions { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// ignore messages from other teams than ours | ||||||
|  | 	if message.Raw.GetData()["team_id"].(string) != b.TeamID { | ||||||
|  | 		b.Log.Debug("message from other team, ignoring") | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// only handle posted, edited or deleted events | ||||||
|  | 	if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model6.WebsocketEventPostEdited || | ||||||
|  | 		message.Raw.EventType() == model6.WebsocketEventPostDeleted) { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmattermost) getVersion() string { | ||||||
|  | 	proto := "https" | ||||||
|  |  | ||||||
|  | 	if b.GetBool("notls") { | ||||||
|  | 		proto = "http" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resp, err := http.Get(proto + "://" + b.GetString("server")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Error("failed getting version") | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  |  | ||||||
|  | 	return resp.Header.Get("X-Version-Id") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmattermost) getChannelID(name string) string { | ||||||
|  | 	idcheck := strings.Split(name, "ID:") | ||||||
|  | 	if len(idcheck) > 1 { | ||||||
|  | 		return idcheck[1] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if b.mc6 != nil { | ||||||
|  | 		return b.mc6.GetChannelID(name, b.TeamID) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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 "" | ||||||
|  | } | ||||||
|   | |||||||
| @@ -3,29 +3,43 @@ package bmattermost | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/42wim/matterbridge/matterclient" | 	"github.com/42wim/matterbridge/matterclient" | ||||||
| 	"github.com/42wim/matterbridge/matterhook" | 	"github.com/42wim/matterbridge/matterhook" | ||||||
|  | 	matterclient6 "github.com/matterbridge/matterclient" | ||||||
| 	"github.com/rs/xid" | 	"github.com/rs/xid" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Bmattermost struct { | type Bmattermost struct { | ||||||
| 	mh     *matterhook.Client | 	mh     *matterhook.Client | ||||||
| 	mc     *matterclient.MMClient | 	mc     *matterclient.MMClient | ||||||
|  | 	mc6    *matterclient6.Client | ||||||
|  | 	v6     bool | ||||||
| 	uuid   string | 	uuid   string | ||||||
| 	TeamID string | 	TeamID string | ||||||
| 	*bridge.Config | 	*bridge.Config | ||||||
| 	avatarMap map[string]string | 	avatarMap      map[string]string | ||||||
|  | 	channelsMutex  sync.RWMutex | ||||||
|  | 	channelInfoMap map[string]*config.ChannelInfo | ||||||
| } | } | ||||||
|  |  | ||||||
| const mattermostPlugin = "mattermost.plugin" | const mattermostPlugin = "mattermost.plugin" | ||||||
|  |  | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
| 	b := &Bmattermost{Config: cfg, avatarMap: make(map[string]string)} | 	b := &Bmattermost{ | ||||||
|  | 		Config:         cfg, | ||||||
|  | 		avatarMap:      make(map[string]string), | ||||||
|  | 		channelInfoMap: make(map[string]*config.ChannelInfo), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.v6 = b.GetBool("v6") | ||||||
| 	b.uuid = xid.New().String() | 	b.uuid = xid.New().String() | ||||||
|  |  | ||||||
| 	return b | 	return b | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -37,6 +51,13 @@ func (b *Bmattermost) Connect() error { | |||||||
| 	if b.Account == mattermostPlugin { | 	if b.Account == mattermostPlugin { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if strings.HasPrefix(b.getVersion(), "6.") { | ||||||
|  | 		if !b.v6 { | ||||||
|  | 			b.v6 = true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if b.GetString("WebhookBindAddress") != "" { | 	if b.GetString("WebhookBindAddress") != "" { | ||||||
| 		if err := b.doConnectWebhookBind(); err != nil { | 		if err := b.doConnectWebhookBind(); err != nil { | ||||||
| 			return err | 			return err | ||||||
| @@ -53,16 +74,34 @@ func (b *Bmattermost) Connect() error { | |||||||
| 		return nil | 		return nil | ||||||
| 	case b.GetString("Token") != "": | 	case b.GetString("Token") != "": | ||||||
| 		b.Log.Info("Connecting using token (sending and receiving)") | 		b.Log.Info("Connecting using token (sending and receiving)") | ||||||
| 		err := b.apiLogin() | 		b.Log.Infof("Using mattermost v6 methods: %t", b.v6) | ||||||
| 		if err != nil { |  | ||||||
| 			return err | 		if b.v6 { | ||||||
|  | 			err := b.apiLogin6() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			err := b.apiLogin() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 		go b.handleMatter() | 		go b.handleMatter() | ||||||
| 	case b.GetString("Login") != "": | 	case b.GetString("Login") != "": | ||||||
| 		b.Log.Info("Connecting using login/password (sending and receiving)") | 		b.Log.Info("Connecting using login/password (sending and receiving)") | ||||||
| 		err := b.apiLogin() | 		b.Log.Infof("Using mattermost v6 methods: %t", b.v6) | ||||||
| 		if err != nil { |  | ||||||
| 			return err | 		if b.v6 { | ||||||
|  | 			err := b.apiLogin6() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			err := b.apiLogin() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 		go b.handleMatter() | 		go b.handleMatter() | ||||||
| 	} | 	} | ||||||
| @@ -81,14 +120,25 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error { | |||||||
| 	if b.Account == mattermostPlugin { | 	if b.Account == mattermostPlugin { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	b.channelsMutex.Lock() | ||||||
|  | 	b.channelInfoMap[channel.ID] = &channel | ||||||
|  | 	b.channelsMutex.Unlock() | ||||||
|  |  | ||||||
| 	// we can only join channels using the API | 	// we can only join channels using the API | ||||||
| 	if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" { | 	if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" { | ||||||
| 		id := b.mc.GetChannelId(channel.Name, b.TeamID) | 		id := b.getChannelID(channel.Name) | ||||||
| 		if id == "" { | 		if id == "" { | ||||||
| 			return fmt.Errorf("Could not find channel ID for channel %s", channel.Name) | 			return fmt.Errorf("Could not find channel ID for channel %s", channel.Name) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if b.mc6 != nil { | ||||||
|  | 			return b.mc6.JoinChannel(id) // nolint:wrapcheck | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		return b.mc.JoinChannel(id) | 		return b.mc.JoinChannel(id) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -118,20 +168,51 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) { | |||||||
| 		if msg.ID == "" { | 		if msg.ID == "" { | ||||||
| 			return "", nil | 			return "", nil | ||||||
| 		} | 		} | ||||||
|  | 		if b.mc6 != nil { | ||||||
|  | 			return msg.ID, b.mc6.DeleteMessage(msg.ID) // nolint:wrapcheck | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		return msg.ID, b.mc.DeleteMessage(msg.ID) | 		return msg.ID, b.mc.DeleteMessage(msg.ID) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Handle prefix hint for unthreaded messages. | 	// Handle prefix hint for unthreaded messages. | ||||||
| 	if msg.ParentID == "msg-parent-not-found" { | 	if msg.ParentNotFound() { | ||||||
| 		msg.ParentID = "" | 		msg.ParentID = "" | ||||||
| 		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) | 		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// we only can reply to the root of the thread, not to a specific ID (like discord for example does) | ||||||
|  | 	if msg.ParentID != "" { | ||||||
|  | 		if b.mc6 != nil { | ||||||
|  | 			post, _, err := b.mc6.Client.GetPost(msg.ParentID, "") | ||||||
|  | 			if err != nil { | ||||||
|  | 				b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err) | ||||||
|  | 			} | ||||||
|  | 			if post.RootId != "" { | ||||||
|  | 				msg.ParentID = post.RootId | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			post, res := b.mc.Client.GetPost(msg.ParentID, "") | ||||||
|  | 			if res.Error != nil { | ||||||
|  | 				b.Log.Errorf("getting post %s failed: %s", msg.ParentID, res.Error.DetailedError) | ||||||
|  | 			} | ||||||
|  | 			if post.RootId != "" { | ||||||
|  | 				msg.ParentID = post.RootId | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Upload a file if it exists | 	// Upload a file if it exists | ||||||
| 	if msg.Extra != nil { | 	if msg.Extra != nil { | ||||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | ||||||
| 			if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil { | 			if b.mc6 != nil { | ||||||
| 				b.Log.Errorf("PostMessage failed: %s", err) | 				if _, err := b.mc6.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil { | ||||||
|  | 					b.Log.Errorf("PostMessage failed: %s", err) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				if _, err := b.mc.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil { | ||||||
|  | 					b.Log.Errorf("PostMessage failed: %s", err) | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		if len(msg.Extra["file"]) > 0 { | 		if len(msg.Extra["file"]) > 0 { | ||||||
| @@ -146,9 +227,17 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) { | |||||||
|  |  | ||||||
| 	// Edit message if we have an ID | 	// Edit message if we have an ID | ||||||
| 	if msg.ID != "" { | 	if msg.ID != "" { | ||||||
|  | 		if b.mc6 != nil { | ||||||
|  | 			return b.mc6.EditMessage(msg.ID, msg.Text) // nolint:wrapcheck | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		return b.mc.EditMessage(msg.ID, msg.Text) | 		return b.mc.EditMessage(msg.ID, msg.Text) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Post normal message | 	// Post normal message | ||||||
| 	return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID) | 	if b.mc6 != nil { | ||||||
|  | 		return b.mc6.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID) // nolint:wrapcheck | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return b.mc.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										101
									
								
								bridge/msteams/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								bridge/msteams/handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | package bmsteams | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
|  |  | ||||||
|  | 	msgraph "github.com/yaegashi/msgraph.go/beta" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) findFile(weburl string) (string, error) { | ||||||
|  | 	itemRB, err := b.gc.GetDriveItemByURL(b.ctx, weburl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	itemRB.Workbook().Worksheets() | ||||||
|  | 	b.gc.Workbooks() | ||||||
|  | 	item, err := itemRB.Request().Get(b.ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	if url, ok := item.GetAdditionalData("@microsoft.graph.downloadUrl"); ok { | ||||||
|  | 		return url.(string), nil | ||||||
|  | 	} | ||||||
|  | 	return "", nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleDownloadFile handles file download | ||||||
|  | func (b *Bmsteams) handleDownloadFile(rmsg *config.Message, filename, weburl string) error { | ||||||
|  | 	realURL, err := b.findFile(weburl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	// Actually download the file. | ||||||
|  | 	data, err := helper.DownloadFile(realURL) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("download %s failed %#v", weburl, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If a comment is attached to the file(s) it is in the 'Text' field of the teams messge event | ||||||
|  | 	// and should be added as comment to only one of the files. We reset the 'Text' field to ensure | ||||||
|  | 	// that the comment is not duplicated. | ||||||
|  | 	comment := rmsg.Text | ||||||
|  | 	rmsg.Text = "" | ||||||
|  | 	helper.HandleDownloadData(b.Log, rmsg, filename, comment, weburl, data, b.General) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) handleAttachments(rmsg *config.Message, msg msgraph.ChatMessage) { | ||||||
|  | 	for _, a := range msg.Attachments { | ||||||
|  | 		//remove the attachment tags from the text | ||||||
|  | 		rmsg.Text = attachRE.ReplaceAllString(rmsg.Text, "") | ||||||
|  |  | ||||||
|  | 		//handle a code snippet (code block) | ||||||
|  | 		if *a.ContentType == "application/vnd.microsoft.card.codesnippet" { | ||||||
|  | 			b.handleCodeSnippet(rmsg, a) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		//handle the download | ||||||
|  | 		err := b.handleDownloadFile(rmsg, *a.Name, *a.ContentURL) | ||||||
|  | 		if err != nil { | ||||||
|  | 			b.Log.Errorf("download of %s failed: %s", *a.Name, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type AttachContent struct { | ||||||
|  | 	Language       string `json:"language"` | ||||||
|  | 	CodeSnippetURL string `json:"codeSnippetUrl"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) handleCodeSnippet(rmsg *config.Message, attach msgraph.ChatMessageAttachment) { | ||||||
|  | 	var content AttachContent | ||||||
|  | 	err := json.Unmarshal([]byte(*attach.Content), &content) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("unmarshal codesnippet failed: %s", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	s := strings.Split(content.CodeSnippetURL, "/") | ||||||
|  | 	if len(s) != 13 { | ||||||
|  | 		b.Log.Errorf("codesnippetUrl has unexpected size: %s", content.CodeSnippetURL) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	resp, err := b.gc.Teams().Request().Client().Get(content.CodeSnippetURL) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("retrieving snippet content failed:%s", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 	res, err := ioutil.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("reading snippet data failed: %s", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	rmsg.Text = rmsg.Text + "\n```" + content.Language + "\n" + string(res) + "\n```\n" | ||||||
|  | } | ||||||
							
								
								
									
										229
									
								
								bridge/msteams/msteams.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								bridge/msteams/msteams.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,229 @@ | |||||||
|  | package bmsteams | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/42wim/matterbridge/bridge" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
|  | 	"github.com/davecgh/go-spew/spew" | ||||||
|  |  | ||||||
|  | 	"github.com/mattn/godown" | ||||||
|  | 	msgraph "github.com/yaegashi/msgraph.go/beta" | ||||||
|  | 	"github.com/yaegashi/msgraph.go/msauth" | ||||||
|  |  | ||||||
|  | 	"golang.org/x/oauth2" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"} | ||||||
|  | 	attachRE      = regexp.MustCompile(`<attachment id=.*?attachment>`) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Bmsteams struct { | ||||||
|  | 	gc    *msgraph.GraphServiceRequestBuilder | ||||||
|  | 	ctx   context.Context | ||||||
|  | 	botID string | ||||||
|  | 	*bridge.Config | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
|  | 	return &Bmsteams{Config: cfg} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) Connect() error { | ||||||
|  | 	tokenCachePath := b.GetString("sessionFile") | ||||||
|  | 	if tokenCachePath == "" { | ||||||
|  | 		tokenCachePath = "msteams_session.json" | ||||||
|  | 	} | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	m := msauth.NewManager() | ||||||
|  | 	m.LoadFile(tokenCachePath) //nolint:errcheck | ||||||
|  | 	ts, err := m.DeviceAuthorizationGrant(ctx, b.GetString("TenantID"), b.GetString("ClientID"), defaultScopes, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	err = m.SaveFile(tokenCachePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err) | ||||||
|  | 	} | ||||||
|  | 	// make file readable only for matterbridge user | ||||||
|  | 	err = os.Chmod(tokenCachePath, 0o600) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err) | ||||||
|  | 	} | ||||||
|  | 	httpClient := oauth2.NewClient(ctx, ts) | ||||||
|  | 	graphClient := msgraph.NewClient(httpClient) | ||||||
|  | 	b.gc = graphClient | ||||||
|  | 	b.ctx = ctx | ||||||
|  |  | ||||||
|  | 	err = b.setBotID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	b.Log.Info("Connection succeeded") | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) Disconnect() error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error { | ||||||
|  | 	go func(name string) { | ||||||
|  | 		for { | ||||||
|  | 			err := b.poll(name) | ||||||
|  | 			if err != nil { | ||||||
|  | 				b.Log.Errorf("polling failed for %s: %s. retrying in 5 seconds", name, err) | ||||||
|  | 			} | ||||||
|  | 			time.Sleep(time.Second * 5) | ||||||
|  | 		} | ||||||
|  | 	}(channel.Name) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) Send(msg config.Message) (string, error) { | ||||||
|  | 	b.Log.Debugf("=> Receiving %#v", msg) | ||||||
|  | 	if msg.ParentValid() { | ||||||
|  | 		return b.sendReply(msg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Handle prefix hint for unthreaded messages. | ||||||
|  | 	if msg.ParentNotFound() { | ||||||
|  | 		msg.ParentID = "" | ||||||
|  | 		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request() | ||||||
|  | 	text := msg.Username + msg.Text | ||||||
|  | 	content := &msgraph.ItemBody{Content: &text} | ||||||
|  | 	rmsg := &msgraph.ChatMessage{Body: content} | ||||||
|  | 	res, err := ct.Add(b.ctx, rmsg) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return *res.ID, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) sendReply(msg config.Message) (string, error) { | ||||||
|  | 	ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().ID(msg.ParentID).Replies().Request() | ||||||
|  | 	// Handle prefix hint for unthreaded messages. | ||||||
|  |  | ||||||
|  | 	text := msg.Username + msg.Text | ||||||
|  | 	content := &msgraph.ItemBody{Content: &text} | ||||||
|  | 	rmsg := &msgraph.ChatMessage{Body: content} | ||||||
|  | 	res, err := ct.Add(b.ctx, rmsg) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return *res.ID, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) getMessages(channel string) ([]msgraph.ChatMessage, error) { | ||||||
|  | 	ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(channel).Messages().Request() | ||||||
|  | 	rct, err := ct.Get(b.ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	b.Log.Debugf("got %#v messages", len(rct)) | ||||||
|  | 	return rct, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | //nolint:gocognit | ||||||
|  | func (b *Bmsteams) poll(channelName string) error { | ||||||
|  | 	msgmap := make(map[string]time.Time) | ||||||
|  | 	b.Log.Debug("getting initial messages") | ||||||
|  | 	res, err := b.getMessages(channelName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	for _, msg := range res { | ||||||
|  | 		msgmap[*msg.ID] = *msg.CreatedDateTime | ||||||
|  | 		if msg.LastModifiedDateTime != nil { | ||||||
|  | 			msgmap[*msg.ID] = *msg.LastModifiedDateTime | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	time.Sleep(time.Second * 5) | ||||||
|  | 	b.Log.Debug("polling for messages") | ||||||
|  | 	for { | ||||||
|  | 		res, err := b.getMessages(channelName) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		for i := len(res) - 1; i >= 0; i-- { | ||||||
|  | 			msg := res[i] | ||||||
|  | 			if mtime, ok := msgmap[*msg.ID]; ok { | ||||||
|  | 				if mtime == *msg.CreatedDateTime && msg.LastModifiedDateTime == nil { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				if msg.LastModifiedDateTime != nil && mtime == *msg.LastModifiedDateTime { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if b.GetBool("debug") { | ||||||
|  | 				b.Log.Debug("Msg dump: ", spew.Sdump(msg)) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// skip non-user message for now. | ||||||
|  | 			if msg.From == nil || msg.From.User == nil { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if *msg.From.User.ID == b.botID { | ||||||
|  | 				b.Log.Debug("skipping own message") | ||||||
|  | 				msgmap[*msg.ID] = *msg.CreatedDateTime | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			msgmap[*msg.ID] = *msg.CreatedDateTime | ||||||
|  | 			if msg.LastModifiedDateTime != nil { | ||||||
|  | 				msgmap[*msg.ID] = *msg.LastModifiedDateTime | ||||||
|  | 			} | ||||||
|  | 			b.Log.Debugf("<= Sending message from %s on %s to gateway", *msg.From.User.DisplayName, b.Account) | ||||||
|  | 			text := b.convertToMD(*msg.Body.Content) | ||||||
|  | 			rmsg := config.Message{ | ||||||
|  | 				Username: *msg.From.User.DisplayName, | ||||||
|  | 				Text:     text, | ||||||
|  | 				Channel:  channelName, | ||||||
|  | 				Account:  b.Account, | ||||||
|  | 				Avatar:   "", | ||||||
|  | 				UserID:   *msg.From.User.ID, | ||||||
|  | 				ID:       *msg.ID, | ||||||
|  | 				Extra:    make(map[string][]interface{}), | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			b.handleAttachments(&rmsg, msg) | ||||||
|  | 			b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
|  | 			b.Remote <- rmsg | ||||||
|  | 		} | ||||||
|  | 		time.Sleep(time.Second * 5) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) setBotID() error { | ||||||
|  | 	req := b.gc.Me().Request() | ||||||
|  | 	r, err := req.Get(b.ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	b.botID = *r.ID | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmsteams) convertToMD(text string) string { | ||||||
|  | 	if !strings.Contains(text, "<div>") { | ||||||
|  | 		return text | ||||||
|  | 	} | ||||||
|  | 	var sb strings.Builder | ||||||
|  | 	err := godown.Convert(&sb, strings.NewReader(text), nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Couldn't convert message to markdown %s", text) | ||||||
|  | 		return text | ||||||
|  | 	} | ||||||
|  | 	return sb.String() | ||||||
|  | } | ||||||
							
								
								
									
										96
									
								
								bridge/mumble/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								bridge/mumble/handlers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | |||||||
|  | package bmumble | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"strconv" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"layeh.com/gumble/gumble" | ||||||
|  |  | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) { | ||||||
|  | 	b.serverConfigUpdate <- *event | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) { | ||||||
|  | 	sender := "unknown" | ||||||
|  | 	if event.TextMessage.Sender != nil { | ||||||
|  | 		sender = event.TextMessage.Sender.Name | ||||||
|  | 	} | ||||||
|  | 	// If the text message is received before receiving a ServerSync | ||||||
|  | 	// and UserState, Client.Self or Self.Channel are nil | ||||||
|  | 	if event.Client.Self == nil || event.Client.Self.Channel == nil { | ||||||
|  | 		b.Log.Warn("Connection bootstrap not finished, discarding text message") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	// Convert Mumble HTML messages to markdown | ||||||
|  | 	parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Error(err) | ||||||
|  | 	} | ||||||
|  | 	now := time.Now().UTC() | ||||||
|  | 	for i, part := range parts { | ||||||
|  | 		// Construct matterbridge message and pass on to the gateway | ||||||
|  | 		rmsg := config.Message{ | ||||||
|  | 			Channel:  strconv.FormatUint(uint64(event.Client.Self.Channel.ID), 10), | ||||||
|  | 			Username: sender, | ||||||
|  | 			UserID:   sender + "@" + b.Host, | ||||||
|  | 			Account:  b.Account, | ||||||
|  | 		} | ||||||
|  | 		if part.Image == nil { | ||||||
|  | 			rmsg.Text = part.Text | ||||||
|  | 		} else { | ||||||
|  | 			fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + part.FileExtension | ||||||
|  | 			rmsg.Extra = make(map[string][]interface{}) | ||||||
|  | 			if err = helper.HandleDownloadSize(b.Log, &rmsg, fname, int64(len(part.Image)), b.General); err != nil { | ||||||
|  | 				b.Log.WithError(err).Warn("not including image in message") | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			helper.HandleDownloadData(b.Log, &rmsg, fname, "", "", &part.Image, b.General) | ||||||
|  | 		} | ||||||
|  | 		b.Log.Debugf("Sending message to gateway: %+v", rmsg) | ||||||
|  | 		b.Remote <- rmsg | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) { | ||||||
|  | 	// Set the user's "bio"/comment | ||||||
|  | 	if comment := b.GetString("UserComment"); comment != "" && event.Client.Self != nil { | ||||||
|  | 		event.Client.Self.SetComment(comment) | ||||||
|  | 	} | ||||||
|  | 	// No need to talk or listen | ||||||
|  | 	event.Client.Self.SetSelfDeafened(true) | ||||||
|  | 	event.Client.Self.SetSelfMuted(true) | ||||||
|  | 	// if the Channel variable is set, this is a reconnect -> rejoin channel | ||||||
|  | 	if b.Channel != nil { | ||||||
|  | 		if err := b.doJoin(event.Client, *b.Channel); err != nil { | ||||||
|  | 			b.Log.Error(err) | ||||||
|  | 		} | ||||||
|  | 		b.Remote <- config.Message{ | ||||||
|  | 			Username: "system", | ||||||
|  | 			Text:     "rejoin", | ||||||
|  | 			Channel:  "", | ||||||
|  | 			Account:  b.Account, | ||||||
|  | 			Event:    config.EventRejoinChannels, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) { | ||||||
|  | 	// Only care about changes to self | ||||||
|  | 	if event.User != event.Client.Self { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	// Someone attempted to move the user out of the configured channel; attempt to join back | ||||||
|  | 	if b.Channel != nil { | ||||||
|  | 		if err := b.doJoin(event.Client, *b.Channel); err != nil { | ||||||
|  | 			b.Log.Error(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) { | ||||||
|  | 	b.connected <- *event | ||||||
|  | } | ||||||
							
								
								
									
										143
									
								
								bridge/mumble/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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 | ||||||
|  | } | ||||||
							
								
								
									
										262
									
								
								bridge/mumble/mumble.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								bridge/mumble/mumble.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,262 @@ | |||||||
|  | package bmumble | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"crypto/x509" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"net" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"layeh.com/gumble/gumble" | ||||||
|  | 	"layeh.com/gumble/gumbleutil" | ||||||
|  |  | ||||||
|  | 	"github.com/42wim/matterbridge/bridge" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
|  | 	stripmd "github.com/writeas/go-strip-markdown" | ||||||
|  |  | ||||||
|  | 	// We need to import the 'data' package as an implicit dependency. | ||||||
|  | 	// See: https://godoc.org/github.com/paulrosania/go-charset/charset | ||||||
|  | 	_ "github.com/paulrosania/go-charset/data" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Bmumble struct { | ||||||
|  | 	client             *gumble.Client | ||||||
|  | 	Nick               string | ||||||
|  | 	Host               string | ||||||
|  | 	Channel            *uint32 | ||||||
|  | 	local              chan config.Message | ||||||
|  | 	running            chan error | ||||||
|  | 	connected          chan gumble.DisconnectEvent | ||||||
|  | 	serverConfigUpdate chan gumble.ServerConfigEvent | ||||||
|  | 	serverConfig       gumble.ServerConfigEvent | ||||||
|  | 	tlsConfig          tls.Config | ||||||
|  |  | ||||||
|  | 	*bridge.Config | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
|  | 	b := &Bmumble{} | ||||||
|  | 	b.Config = cfg | ||||||
|  | 	b.Nick = b.GetString("Nick") | ||||||
|  | 	b.local = make(chan config.Message) | ||||||
|  | 	b.running = make(chan error) | ||||||
|  | 	b.connected = make(chan gumble.DisconnectEvent) | ||||||
|  | 	b.serverConfigUpdate = make(chan gumble.ServerConfigEvent) | ||||||
|  | 	return b | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) Connect() error { | ||||||
|  | 	b.Log.Infof("Connecting %s", b.GetString("Server")) | ||||||
|  | 	host, portstr, err := net.SplitHostPort(b.GetString("Server")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	b.Host = host | ||||||
|  | 	_, err = strconv.Atoi(portstr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err = b.buildTLSConfig(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	go b.doSend() | ||||||
|  | 	go b.connectLoop() | ||||||
|  | 	err = <-b.running | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) Disconnect() error { | ||||||
|  | 	return b.client.Disconnect() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error { | ||||||
|  | 	cid, err := strconv.ParseUint(channel.Name, 10, 32) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	channelID := uint32(cid) | ||||||
|  | 	if b.Channel != nil && *b.Channel != channelID { | ||||||
|  | 		b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel) | ||||||
|  | 		return errors.New("the Mumble bridge can only join a single channel") | ||||||
|  | 	} | ||||||
|  | 	b.Channel = &channelID | ||||||
|  | 	return b.doJoin(b.client, channelID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) Send(msg config.Message) (string, error) { | ||||||
|  | 	// Only process text messages | ||||||
|  | 	b.Log.Debugf("=> Received local message %#v", msg) | ||||||
|  | 	if msg.Event != "" && msg.Event != config.EventUserAction { | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	attachments := b.extractFiles(&msg) | ||||||
|  | 	b.local <- msg | ||||||
|  | 	for _, a := range attachments { | ||||||
|  | 		b.local <- a | ||||||
|  | 	} | ||||||
|  | 	return "", nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) buildTLSConfig() error { | ||||||
|  | 	b.tlsConfig = tls.Config{} | ||||||
|  | 	// Load TLS client certificate keypair required for registered user authentication | ||||||
|  | 	if cpath := b.GetString("TLSClientCertificate"); cpath != "" { | ||||||
|  | 		if ckey := b.GetString("TLSClientKey"); ckey != "" { | ||||||
|  | 			cert, err := tls.LoadX509KeyPair(cpath, ckey) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			b.tlsConfig.Certificates = []tls.Certificate{cert} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	// Load TLS CA used for server verification.  If not provided, the Go system trust anchor is used | ||||||
|  | 	if capath := b.GetString("TLSCACertificate"); capath != "" { | ||||||
|  | 		ca, err := ioutil.ReadFile(capath) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		b.tlsConfig.RootCAs = x509.NewCertPool() | ||||||
|  | 		b.tlsConfig.RootCAs.AppendCertsFromPEM(ca) | ||||||
|  | 	} | ||||||
|  | 	b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify") | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) connectLoop() { | ||||||
|  | 	firstConnect := true | ||||||
|  | 	for { | ||||||
|  | 		err := b.doConnect() | ||||||
|  | 		if firstConnect { | ||||||
|  | 			b.running <- err | ||||||
|  | 		} | ||||||
|  | 		if err != nil { | ||||||
|  | 			b.Log.Errorf("Connection to server failed: %#v", err) | ||||||
|  | 			if firstConnect { | ||||||
|  | 				break | ||||||
|  | 			} else { | ||||||
|  | 				b.Log.Info("Retrying in 10s") | ||||||
|  | 				time.Sleep(10 * time.Second) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		firstConnect = false | ||||||
|  | 		d := <-b.connected | ||||||
|  | 		switch d.Type { | ||||||
|  | 		case gumble.DisconnectError: | ||||||
|  | 			b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String) | ||||||
|  | 			continue | ||||||
|  | 		case gumble.DisconnectKicked: | ||||||
|  | 			b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String) | ||||||
|  | 			continue | ||||||
|  | 		case gumble.DisconnectBanned: | ||||||
|  | 			b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String) | ||||||
|  | 			close(b.connected) | ||||||
|  | 			close(b.running) | ||||||
|  | 			return | ||||||
|  | 		case gumble.DisconnectUser: | ||||||
|  | 			b.Log.Infof("Disconnect successful") | ||||||
|  | 			close(b.connected) | ||||||
|  | 			close(b.running) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) doConnect() error { | ||||||
|  | 	// Create new gumble config and attach event handlers | ||||||
|  | 	gumbleConfig := gumble.NewConfig() | ||||||
|  | 	gumbleConfig.Attach(gumbleutil.Listener{ | ||||||
|  | 		ServerConfig: b.handleServerConfig, | ||||||
|  | 		TextMessage:  b.handleTextMessage, | ||||||
|  | 		Connect:      b.handleConnect, | ||||||
|  | 		Disconnect:   b.handleDisconnect, | ||||||
|  | 		UserChange:   b.handleUserChange, | ||||||
|  | 	}) | ||||||
|  | 	gumbleConfig.Username = b.GetString("Nick") | ||||||
|  | 	if password := b.GetString("Password"); password != "" { | ||||||
|  | 		gumbleConfig.Password = password | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	b.client = client | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error { | ||||||
|  | 	channel, ok := client.Channels[channelID] | ||||||
|  | 	if !ok { | ||||||
|  | 		return fmt.Errorf("no channel with ID %d", channelID) | ||||||
|  | 	} | ||||||
|  | 	client.Self.Move(channel) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) doSend() { | ||||||
|  | 	// Message sending loop that makes sure server-side | ||||||
|  | 	// restrictions and client-side message traits don't conflict | ||||||
|  | 	// with each other. | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case serverConfig := <-b.serverConfigUpdate: | ||||||
|  | 			b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength) | ||||||
|  | 			b.serverConfig = serverConfig | ||||||
|  | 		case msg := <-b.local: | ||||||
|  | 			b.processMessage(&msg) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bmumble) processMessage(msg *config.Message) { | ||||||
|  | 	b.Log.Debugf("Processing message %s", msg.Text) | ||||||
|  |  | ||||||
|  | 	allowHTML := true | ||||||
|  | 	if b.serverConfig.AllowHTML != nil { | ||||||
|  | 		allowHTML = *b.serverConfig.AllowHTML | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If this is a specially generated image message, send it unmodified | ||||||
|  | 	if msg.Event == "mumble_image" { | ||||||
|  | 		if allowHTML { | ||||||
|  | 			b.client.Self.Channel.Send(msg.Username+msg.Text, false) | ||||||
|  | 		} else { | ||||||
|  | 			b.Log.Info("Can't send image, server does not allow HTML messages") | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Don't process empty messages | ||||||
|  | 	if len(msg.Text) == 0 { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	// If HTML is allowed, convert markdown into HTML, otherwise strip markdown | ||||||
|  | 	if allowHTML { | ||||||
|  | 		msg.Text = helper.ParseMarkdown(msg.Text) | ||||||
|  | 	} else { | ||||||
|  | 		msg.Text = stripmd.Strip(msg.Text) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If there is a maximum message length, split and truncate the lines | ||||||
|  | 	var msgLines []string | ||||||
|  | 	if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil { | ||||||
|  | 		msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped")) | ||||||
|  | 	} else { | ||||||
|  | 		msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped")) | ||||||
|  | 	} | ||||||
|  | 	// Send the individual lines | ||||||
|  | 	for i := range msgLines { | ||||||
|  | 		// Remove unnecessary newline character, since either way we're sending it as individual lines | ||||||
|  | 		msgLines[i] = strings.TrimSuffix(msgLines[i], "\n") | ||||||
|  | 		b.client.Self.Channel.Send(msg.Username+msgLines[i], false) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										295
									
								
								bridge/nctalk/nctalk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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 | package brocketchat | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
|  | 	"github.com/matterbridge/Rocket.Chat.Go.SDK/models" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (b *Brocketchat) handleRocket() { | func (b *Brocketchat) handleRocket() { | ||||||
| @@ -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) { | func (b *Brocketchat) handleRocketClient(messages chan *config.Message) { | ||||||
| 	for message := range b.messageChan { | 	for message := range b.messageChan { | ||||||
|  | 		message := message | ||||||
| 		// skip messages with same ID, apparently messages get duplicated for an unknown reason | 		// skip messages with same ID, apparently messages get duplicated for an unknown reason | ||||||
| 		if _, ok := b.cache.Get(message.ID); ok { | 		if _, ok := b.cache.Get(message.ID); ok { | ||||||
| 			continue | 			continue | ||||||
| @@ -58,11 +80,51 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) { | |||||||
| 			Account:  b.Account, | 			Account:  b.Account, | ||||||
| 			UserID:   message.User.ID, | 			UserID:   message.User.ID, | ||||||
| 			ID:       message.ID, | 			ID:       message.ID, | ||||||
|  | 			Extra:    make(map[string][]interface{}), | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		b.handleAttachments(&message, rmsg) | ||||||
|  |  | ||||||
|  | 		// handleStatusEvent returns false if the message should be dropped | ||||||
|  | 		// in that case it is probably some modification to the channel we do not want to relay | ||||||
|  | 		if b.handleStatusEvent(m, rmsg) { | ||||||
|  | 			messages <- rmsg | ||||||
| 		} | 		} | ||||||
| 		messages <- rmsg |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (b *Brocketchat) handleAttachments(message *models.Message, rmsg *config.Message) { | ||||||
|  | 	if rmsg.Text == "" { | ||||||
|  | 		for _, attachment := range message.Attachments { | ||||||
|  | 			if attachment.Title != "" { | ||||||
|  | 				rmsg.Text = attachment.Title + "\n" | ||||||
|  | 			} | ||||||
|  | 			if attachment.Title != "" && attachment.Text != "" { | ||||||
|  | 				rmsg.Text += "\n" | ||||||
|  | 			} | ||||||
|  | 			if attachment.Text != "" { | ||||||
|  | 				rmsg.Text += attachment.Text | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i := range message.Attachments { | ||||||
|  | 		if err := b.handleDownloadFile(rmsg, &message.Attachments[i]); err != nil { | ||||||
|  | 			b.Log.Errorf("Could not download incoming file: %#v", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Brocketchat) handleDownloadFile(rmsg *config.Message, file *models.Attachment) error { | ||||||
|  | 	downloadURL := b.GetString("server") + file.TitleLink | ||||||
|  | 	data, err := helper.DownloadFileAuthRocket(downloadURL, b.user.Token, b.user.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("download %s failed %#v", downloadURL, err) | ||||||
|  | 	} | ||||||
|  | 	helper.HandleDownloadData(b.Log, rmsg, file.Title, rmsg.Text, downloadURL, data, b.General) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (b *Brocketchat) handleUploadFile(msg *config.Message) error { | func (b *Brocketchat) handleUploadFile(msg *config.Message) error { | ||||||
| 	for _, f := range msg.Extra["file"] { | 	for _, f := range msg.Extra["file"] { | ||||||
| 		fi := f.(config.FileInfo) | 		fi := f.(config.FileInfo) | ||||||
|   | |||||||
| @@ -29,6 +29,12 @@ type Brocketchat struct { | |||||||
| 	sync.RWMutex | 	sync.RWMutex | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	sUserJoined       = "uj" | ||||||
|  | 	sUserLeft         = "ul" | ||||||
|  | 	sRoomChangedTopic = "room_changed_topic" | ||||||
|  | ) | ||||||
|  |  | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
| 	newCache, err := lru.New(100) | 	newCache, err := lru.New(100) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -1,18 +1,22 @@ | |||||||
| package bslack | package bslack | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html" | 	"html" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/nlopes/slack" | 	"github.com/slack-go/slack" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // ErrEventIgnored is for events that should be ignored | ||||||
|  | var ErrEventIgnored = errors.New("this event message should ignored") | ||||||
|  |  | ||||||
| func (b *Bslack) handleSlack() { | func (b *Bslack) handleSlack() { | ||||||
| 	messages := make(chan *config.Message) | 	messages := make(chan *config.Message) | ||||||
| 	if b.GetString(incomingWebhookConfig) != "" { | 	if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" { | ||||||
| 		b.Log.Debugf("Choosing webhooks based receiving") | 		b.Log.Debugf("Choosing webhooks based receiving") | ||||||
| 		go b.handleMatterHook(messages) | 		go b.handleMatterHook(messages) | ||||||
| 	} else { | 	} else { | ||||||
| @@ -23,7 +27,8 @@ func (b *Bslack) handleSlack() { | |||||||
| 	b.Log.Debug("Start listening for Slack messages") | 	b.Log.Debug("Start listening for Slack messages") | ||||||
| 	for message := range messages { | 	for message := range messages { | ||||||
| 		// don't do any action on deleted/typing 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) | 			b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) | ||||||
| 			// cleanup the message | 			// cleanup the message | ||||||
| 			message.Text = b.replaceMention(message.Text) | 			message.Text = b.replaceMention(message.Text) | ||||||
| @@ -53,7 +58,9 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) { | |||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 			rmsg, err := b.handleTypingEvent(ev) | 			rmsg, err := b.handleTypingEvent(ev) | ||||||
| 			if err != nil { | 			if err == ErrEventIgnored { | ||||||
|  | 				continue | ||||||
|  | 			} else if err != nil { | ||||||
| 				b.Log.Errorf("%#v", err) | 				b.Log.Errorf("%#v", err) | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| @@ -70,6 +77,13 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) { | |||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 			messages <- rmsg | 			messages <- rmsg | ||||||
|  | 		case *slack.FileDeletedEvent: | ||||||
|  | 			rmsg, err := b.handleFileDeletedEvent(ev) | ||||||
|  | 			if err != nil { | ||||||
|  | 				b.Log.Errorf("%#v", err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			messages <- rmsg | ||||||
| 		case *slack.OutgoingErrorEvent: | 		case *slack.OutgoingErrorEvent: | ||||||
| 			b.Log.Debugf("%#v", ev.Error()) | 			b.Log.Debugf("%#v", ev.Error()) | ||||||
| 		case *slack.ChannelJoinedEvent: | 		case *slack.ChannelJoinedEvent: | ||||||
| @@ -87,8 +101,10 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) { | |||||||
| 			b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) | 			b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) | ||||||
| 		case *slack.MemberJoinedChannelEvent: | 		case *slack.MemberJoinedChannelEvent: | ||||||
| 			b.users.populateUser(ev.User) | 			b.users.populateUser(ev.User) | ||||||
| 		case *slack.HelloEvent, *slack.LatencyReport: | 		case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent: | ||||||
| 			continue | 			continue | ||||||
|  | 		case *slack.UserChangeEvent: | ||||||
|  | 			b.users.invalidateUser(ev.User.ID) | ||||||
| 		default: | 		default: | ||||||
| 			b.Log.Debugf("Unhandled incoming event: %T", ev) | 			b.Log.Debugf("Unhandled incoming event: %T", ev) | ||||||
| 		} | 		} | ||||||
| @@ -124,11 +140,11 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Skip any messages that we made ourselves or from 'slackbot' (see #527). | 	// Check for our callback ID | ||||||
| 	if ev.Username == sSlackBotUser || | 	hasOurCallbackID := false | ||||||
| 		(b.rtm != nil && ev.Username == b.si.User.Name) || | 	if len(ev.Blocks.BlockSet) == 1 { | ||||||
| 		(len(ev.Attachments) > 0 && ev.Attachments[0].CallbackID == "matterbridge_"+b.uuid) { | 		block, ok := ev.Blocks.BlockSet[0].(*slack.SectionBlock) | ||||||
| 		return true | 		hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if ev.SubMessage != nil { | 	if ev.SubMessage != nil { | ||||||
| @@ -143,6 +159,16 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { | |||||||
| 		if ev.SubType == "message_replied" && ev.Hidden { | 		if ev.SubType == "message_replied" && ev.Hidden { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
|  | 		if len(ev.SubMessage.Blocks.BlockSet) == 1 { | ||||||
|  | 			block, ok := ev.SubMessage.Blocks.BlockSet[0].(*slack.SectionBlock) | ||||||
|  | 			hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Skip any messages that we made ourselves or from 'slackbot' (see #527). | ||||||
|  | 	if ev.Username == sSlackBotUser || | ||||||
|  | 		(b.rtm != nil && ev.Username == b.si.User.Name) || hasOurCallbackID { | ||||||
|  | 		return true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(ev.Files) > 0 { | 	if len(ev.Files) > 0 { | ||||||
| @@ -204,6 +230,26 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er | |||||||
| 	return rmsg, nil | 	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 { | func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool { | ||||||
| 	switch ev.SubType { | 	switch ev.SubType { | ||||||
| 	case sChannelJoined, sMemberJoined: | 	case sChannelJoined, sMemberJoined: | ||||||
| @@ -263,6 +309,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. | 	// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra. | ||||||
| 	for i := range ev.Files { | 	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 { | 		if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil { | ||||||
| 			b.Log.Errorf("Could not download incoming file: %#v", err) | 			b.Log.Errorf("Could not download incoming file: %#v", err) | ||||||
| 		} | 		} | ||||||
| @@ -270,6 +318,9 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) | |||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) { | func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) { | ||||||
|  | 	if ev.User == b.si.User.ID { | ||||||
|  | 		return nil, ErrEventIgnored | ||||||
|  | 	} | ||||||
| 	channelInfo, err := b.channels.getChannelByID(ev.Channel) | 	channelInfo, err := b.channels.getChannelByID(ev.Channel) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -309,7 +360,7 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retr | |||||||
| 	// that the comment is not duplicated. | 	// that the comment is not duplicated. | ||||||
| 	comment := rmsg.Text | 	comment := rmsg.Text | ||||||
| 	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 | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,8 +7,8 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/nlopes/slack" |  | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/slack-go/slack" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // populateReceivedMessage shapes the initial Matterbridge message that we will forward to the | // populateReceivedMessage shapes the initial Matterbridge message that we will forward to the | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/matterhook" | 	"github.com/42wim/matterbridge/matterhook" | ||||||
| 	"github.com/nlopes/slack" | 	"github.com/slack-go/slack" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type BLegacy struct { | type BLegacy struct { | ||||||
|   | |||||||
| @@ -13,8 +13,8 @@ import ( | |||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/42wim/matterbridge/matterhook" | 	"github.com/42wim/matterbridge/matterhook" | ||||||
| 	lru "github.com/hashicorp/golang-lru" | 	lru "github.com/hashicorp/golang-lru" | ||||||
| 	"github.com/nlopes/slack" |  | ||||||
| 	"github.com/rs/xid" | 	"github.com/rs/xid" | ||||||
|  | 	"github.com/slack-go/slack" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Bslack struct { | type Bslack struct { | ||||||
| @@ -36,24 +36,25 @@ type Bslack struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	sHello           = "hello" | 	sHello               = "hello" | ||||||
| 	sChannelJoin     = "channel_join" | 	sChannelJoin         = "channel_join" | ||||||
| 	sChannelLeave    = "channel_leave" | 	sChannelLeave        = "channel_leave" | ||||||
| 	sChannelJoined   = "channel_joined" | 	sChannelJoined       = "channel_joined" | ||||||
| 	sMemberJoined    = "member_joined_channel" | 	sMemberJoined        = "member_joined_channel" | ||||||
| 	sMessageChanged  = "message_changed" | 	sMessageChanged      = "message_changed" | ||||||
| 	sMessageDeleted  = "message_deleted" | 	sMessageDeleted      = "message_deleted" | ||||||
| 	sSlackAttachment = "slack_attachment" | 	sSlackAttachment     = "slack_attachment" | ||||||
| 	sPinnedItem      = "pinned_item" | 	sPinnedItem          = "pinned_item" | ||||||
| 	sUnpinnedItem    = "unpinned_item" | 	sUnpinnedItem        = "unpinned_item" | ||||||
| 	sChannelTopic    = "channel_topic" | 	sChannelTopic        = "channel_topic" | ||||||
| 	sChannelPurpose  = "channel_purpose" | 	sChannelPurpose      = "channel_purpose" | ||||||
| 	sFileComment     = "file_comment" | 	sFileComment         = "file_comment" | ||||||
| 	sMeMessage       = "me_message" | 	sMeMessage           = "me_message" | ||||||
| 	sUserTyping      = "user_typing" | 	sUserTyping          = "user_typing" | ||||||
| 	sLatencyReport   = "latency_report" | 	sLatencyReport       = "latency_report" | ||||||
| 	sSystemUser      = "system" | 	sSystemUser          = "system" | ||||||
| 	sSlackBotUser    = "slackbot" | 	sSlackBotUser        = "slackbot" | ||||||
|  | 	cfileDownloadChannel = "file_download_channel" | ||||||
|  |  | ||||||
| 	tokenConfig           = "Token" | 	tokenConfig           = "Token" | ||||||
| 	incomingWebhookConfig = "WebhookBindAddress" | 	incomingWebhookConfig = "WebhookBindAddress" | ||||||
| @@ -64,6 +65,7 @@ const ( | |||||||
| 	editSuffixConfig      = "EditSuffix" | 	editSuffixConfig      = "EditSuffix" | ||||||
| 	iconURLConfig         = "iconurl" | 	iconURLConfig         = "iconurl" | ||||||
| 	noSendJoinConfig      = "nosendjoinpart" | 	noSendJoinConfig      = "nosendjoinpart" | ||||||
|  | 	messageLength         = 3000 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
| @@ -155,7 +157,7 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error { | |||||||
|  |  | ||||||
| 	// try to join a channel when in legacy | 	// try to join a channel when in legacy | ||||||
| 	if b.legacy { | 	if b.legacy { | ||||||
| 		_, err := b.sc.JoinChannel(channel.Name) | 		_, _, _, err := b.sc.JoinConversation(channel.Name) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			switch err.Error() { | 			switch err.Error() { | ||||||
| 			case "name_taken", "restricted_action": | 			case "name_taken", "restricted_action": | ||||||
| @@ -194,6 +196,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) { | |||||||
| 		b.Log.Debugf("=> Receiving %#v", msg) | 		b.Log.Debugf("=> Receiving %#v", msg) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	msg.Text = helper.ClipMessage(msg.Text, messageLength, b.GetString("MessageClipped")) | ||||||
| 	msg.Text = b.replaceCodeFence(msg.Text) | 	msg.Text = b.replaceCodeFence(msg.Text) | ||||||
|  |  | ||||||
| 	// Make a action /me of the message | 	// Make a action /me of the message | ||||||
| @@ -202,7 +205,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Use webhook to send the message | 	// Use webhook to send the message | ||||||
| 	if b.GetString(outgoingWebhookConfig) != "" { | 	if b.GetString(outgoingWebhookConfig) != "" && b.GetString(tokenConfig) == "" { | ||||||
| 		return "", b.sendWebhook(msg) | 		return "", b.sendWebhook(msg) | ||||||
| 	} | 	} | ||||||
| 	return b.sendRTM(msg) | 	return b.sendRTM(msg) | ||||||
| @@ -297,7 +300,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Handle prefix hint for unthreaded messages. | 	// Handle prefix hint for unthreaded messages. | ||||||
| 	if msg.ParentID == "msg-parent-not-found" { | 	if msg.ParentNotFound() { | ||||||
| 		msg.ParentID = "" | 		msg.ParentID = "" | ||||||
| 		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) | 		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) | ||||||
| 	} | 	} | ||||||
| @@ -408,7 +411,6 @@ func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (b | |||||||
| 	} | 	} | ||||||
| 	messageOptions := b.prepareMessageOptions(msg) | 	messageOptions := b.prepareMessageOptions(msg) | ||||||
| 	for { | 	for { | ||||||
| 		messageOptions = append(messageOptions, slack.MsgOptionText(msg.Text, false)) |  | ||||||
| 		_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...) | 		_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...) | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
| 			return true, nil | 			return true, nil | ||||||
| @@ -427,11 +429,6 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s | |||||||
| 		return "", nil | 		return "", nil | ||||||
| 	} | 	} | ||||||
| 	messageOptions := b.prepareMessageOptions(msg) | 	messageOptions := b.prepareMessageOptions(msg) | ||||||
| 	messageOptions = append( |  | ||||||
| 		messageOptions, |  | ||||||
| 		slack.MsgOptionText(msg.Text, false), |  | ||||||
| 		slack.MsgOptionEnableLinkUnfurl(), |  | ||||||
| 	) |  | ||||||
| 	for { | 	for { | ||||||
| 		_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...) | 		_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...) | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
| @@ -463,7 +460,7 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) { | |||||||
| 		b.cache.Add("filename"+fi.Name, ts) | 		b.cache.Add("filename"+fi.Name, ts) | ||||||
| 		initialComment := fmt.Sprintf("File from %s", msg.Username) | 		initialComment := fmt.Sprintf("File from %s", msg.Username) | ||||||
| 		if fi.Comment != "" { | 		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{ | 		res, err := b.sc.UploadFile(slack.FileUploadParameters{ | ||||||
| 			Reader:          bytes.NewReader(*fi.Data), | 			Reader:          bytes.NewReader(*fi.Data), | ||||||
| @@ -497,8 +494,6 @@ func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var attachments []slack.Attachment | 	var attachments []slack.Attachment | ||||||
| 	// add a callback ID so we can see we created it |  | ||||||
| 	attachments = append(attachments, slack.Attachment{CallbackID: "matterbridge_" + b.uuid}) |  | ||||||
| 	// add file attachments | 	// add file attachments | ||||||
| 	attachments = append(attachments, b.createAttach(msg.Extra)...) | 	attachments = append(attachments, b.createAttach(msg.Extra)...) | ||||||
| 	// add slack attachments (from another slack bridge) | 	// add slack attachments (from another slack bridge) | ||||||
| @@ -509,6 +504,19 @@ func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var opts []slack.MsgOption | 	var opts []slack.MsgOption | ||||||
|  | 	opts = append(opts, | ||||||
|  | 		// provide regular text field (fallback used in Slack notifications, etc.) | ||||||
|  | 		slack.MsgOptionText(msg.Text, false), | ||||||
|  |  | ||||||
|  | 		// add a callback ID so we can see we created it | ||||||
|  | 		slack.MsgOptionBlocks(slack.NewSectionBlock( | ||||||
|  | 			slack.NewTextBlockObject(slack.MarkdownType, msg.Text, false, false), | ||||||
|  | 			nil, nil, | ||||||
|  | 			slack.SectionBlockOptionBlockID("matterbridge_"+b.uuid), | ||||||
|  | 		)), | ||||||
|  |  | ||||||
|  | 		slack.MsgOptionEnableLinkUnfurl(), | ||||||
|  | 	) | ||||||
| 	opts = append(opts, slack.MsgOptionAttachments(attachments...)) | 	opts = append(opts, slack.MsgOptionAttachments(attachments...)) | ||||||
| 	opts = append(opts, slack.MsgOptionPostMessageParameters(params)) | 	opts = append(opts, slack.MsgOptionPostMessageParameters(params)) | ||||||
| 	return opts | 	return opts | ||||||
|   | |||||||
| @@ -8,8 +8,8 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/nlopes/slack" |  | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/slack-go/slack" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const minimumRefreshInterval = 10 * time.Second | const minimumRefreshInterval = 10 * time.Second | ||||||
| @@ -113,6 +113,12 @@ func (b *users) populateUser(userID string) { | |||||||
| 	b.users[userID] = user | 	b.users[userID] = user | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (b *users) invalidateUser(userID string) { | ||||||
|  | 	b.usersMutex.Lock() | ||||||
|  | 	defer b.usersMutex.Unlock() | ||||||
|  | 	delete(b.users, userID) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (b *users) populateUsers(wait bool) { | func (b *users) populateUsers(wait bool) { | ||||||
| 	b.refreshMutex.Lock() | 	b.refreshMutex.Lock() | ||||||
| 	if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) { | 	if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) { | ||||||
| @@ -283,8 +289,9 @@ func (b *channels) populateChannels(wait bool) { | |||||||
| 	// We only retrieve public and private channels, not IMs | 	// We only retrieve public and private channels, not IMs | ||||||
| 	// and MPIMs as those do not have a channel name. | 	// and MPIMs as those do not have a channel name. | ||||||
| 	queryParams := &slack.GetConversationsParameters{ | 	queryParams := &slack.GetConversationsParameters{ | ||||||
| 		ExcludeArchived: "true", | 		ExcludeArchived: true, | ||||||
| 		Types:           []string{"public_channel,private_channel"}, | 		Types:           []string{"public_channel,private_channel"}, | ||||||
|  | 		Limit:           1000, | ||||||
| 	} | 	} | ||||||
| 	for { | 	for { | ||||||
| 		channels, nextCursor, err := b.sc.GetConversations(queryParams) | 		channels, nextCursor, err := b.sc.GetConversations(queryParams) | ||||||
|   | |||||||
| @@ -130,6 +130,10 @@ func (b *Bsshchat) handleSSHChat() error { | |||||||
| 			if strings.Contains(b.r.Text(), "Rate limiting is in effect") { | 			if strings.Contains(b.r.Text(), "Rate limiting is in effect") { | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  | 			// skip our own messages | ||||||
|  | 			if !strings.HasPrefix(b.r.Text(), "["+b.GetString("Nick")+"] \x1b") { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
| 			res := strings.Split(stripPrompt(b.r.Text()), ":") | 			res := strings.Split(stripPrompt(b.r.Text()), ":") | ||||||
| 			if res[0] == "-> Set theme" { | 			if res[0] == "-> Set theme" { | ||||||
| 				wait = false | 				wait = false | ||||||
|   | |||||||
| @@ -1,22 +1,36 @@ | |||||||
| package btelegram | package btelegram | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"html" | 	"html" | ||||||
| 	"regexp" | 	"path/filepath" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"unicode/utf16" | 	"unicode/utf16" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" | 	"github.com/davecgh/go-spew/spew" | ||||||
|  | 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message { | func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message { | ||||||
| 	// handle channels | 	// handle channels | ||||||
| 	if posted != nil { | 	if posted != nil { | ||||||
| 		message = posted | 		if posted.Text == "/chatId" { | ||||||
| 		rmsg.Text = message.Text | 			chatID := strconv.FormatInt(posted.Chat.ID, 10) | ||||||
|  |  | ||||||
|  | 			_, 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 | 	// edited channel message | ||||||
| @@ -39,22 +53,32 @@ func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message | |||||||
|  |  | ||||||
| // handleForwarded handles forwarded messages | // handleForwarded handles forwarded messages | ||||||
| func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) { | func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) { | ||||||
| 	if message.ForwardFrom != nil { | 	if message.ForwardDate == 0 { | ||||||
| 		usernameForward := "" | 		return | ||||||
| 		if b.GetBool("UseFirstName") { | 	} | ||||||
|  |  | ||||||
|  | 	if message.ForwardFrom == nil { | ||||||
|  | 		rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	usernameForward := "" | ||||||
|  | 	if b.GetBool("UseFirstName") { | ||||||
|  | 		usernameForward = message.ForwardFrom.FirstName | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if usernameForward == "" { | ||||||
|  | 		usernameForward = message.ForwardFrom.UserName | ||||||
|  | 		if usernameForward == "" { | ||||||
| 			usernameForward = message.ForwardFrom.FirstName | 			usernameForward = message.ForwardFrom.FirstName | ||||||
| 		} | 		} | ||||||
| 		if usernameForward == "" { |  | ||||||
| 			usernameForward = message.ForwardFrom.UserName |  | ||||||
| 			if usernameForward == "" { |  | ||||||
| 				usernameForward = message.ForwardFrom.FirstName |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		if usernameForward == "" { |  | ||||||
| 			usernameForward = unknownUser |  | ||||||
| 		} |  | ||||||
| 		rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if usernameForward == "" { | ||||||
|  | 		usernameForward = unknownUser | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text | ||||||
| } | } | ||||||
|  |  | ||||||
| // handleQuoting handles quoting of previous messages | // handleQuoting handles quoting of previous messages | ||||||
| @@ -84,7 +108,7 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag | |||||||
| // handleUsername handles the correct setting of the username | // handleUsername handles the correct setting of the username | ||||||
| func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) { | func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) { | ||||||
| 	if message.From != nil { | 	if message.From != nil { | ||||||
| 		rmsg.UserID = strconv.Itoa(message.From.ID) | 		rmsg.UserID = strconv.FormatInt(message.From.ID, 10) | ||||||
| 		if b.GetBool("UseFirstName") { | 		if b.GetBool("UseFirstName") { | ||||||
| 			rmsg.Username = message.From.FirstName | 			rmsg.Username = message.From.FirstName | ||||||
| 		} | 		} | ||||||
| @@ -95,11 +119,30 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		// only download avatars if we have a place to upload them (configured mediaserver) | 		// only download avatars if we have a place to upload them (configured mediaserver) | ||||||
| 		if b.General.MediaServerUpload != "" { | 		if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") { | ||||||
| 			b.handleDownloadAvatar(message.From.ID, rmsg.Channel) | 			b.handleDownloadAvatar(message.From.ID, rmsg.Channel) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if message.SenderChat != nil { //nolint:nestif | ||||||
|  | 		rmsg.UserID = strconv.FormatInt(message.SenderChat.ID, 10) | ||||||
|  | 		if b.GetBool("UseFirstName") { | ||||||
|  | 			rmsg.Username = message.SenderChat.FirstName | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		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) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// if we really didn't find a username, set it to unknown | 	// if we really didn't find a username, set it to unknown | ||||||
| 	if rmsg.Username == "" { | 	if rmsg.Username == "" { | ||||||
| 		rmsg.Username = unknownUser | 		rmsg.Username = unknownUser | ||||||
| @@ -116,6 +159,10 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { | |||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if b.GetInt("debuglevel") == 1 { | ||||||
|  | 			spew.Dump(update.Message) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		var message *tgbotapi.Message | 		var message *tgbotapi.Message | ||||||
|  |  | ||||||
| 		rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})} | 		rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})} | ||||||
| @@ -157,7 +204,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { | |||||||
| 			rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text) | 			rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text) | ||||||
| 			// channels don't have (always?) user information. see #410 | 			// channels don't have (always?) user information. see #410 | ||||||
| 			if message.From != nil { | 			if message.From != nil { | ||||||
| 				rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.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) | 			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) | ||||||
| @@ -170,39 +217,73 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { | |||||||
| // handleDownloadAvatar downloads the avatar of userid from channel | // handleDownloadAvatar downloads the avatar of userid from channel | ||||||
| // sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. | // sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. | ||||||
| // logs an error message if it fails | // logs an error message if it fails | ||||||
| func (b *Btelegram) handleDownloadAvatar(userid int, channel string) { | func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) { | ||||||
| 	rmsg := config.Message{Username: "system", | 	rmsg := config.Message{ | ||||||
| 		Text:    "avatar", | 		Username: "system", | ||||||
| 		Channel: channel, | 		Text:     "avatar", | ||||||
| 		Account: b.Account, | 		Channel:  channel, | ||||||
| 		UserID:  strconv.Itoa(userid), | 		Account:  b.Account, | ||||||
| 		Event:   config.EventAvatarDownload, | 		UserID:   strconv.FormatInt(userid, 10), | ||||||
| 		Extra:   make(map[string][]interface{})} | 		Event:    config.EventAvatarDownload, | ||||||
|  | 		Extra:    make(map[string][]interface{}), | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok { | 	if _, ok := b.avatarMap[strconv.FormatInt(userid, 10)]; ok { | ||||||
| 		photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1}) | 		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 { | 		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 { | func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) { | ||||||
| 			photo := photos.Photos[0][0] | 	format := b.GetString("MediaConvertTgs") | ||||||
| 			url := b.getFileDirectURL(photo.FileID) | 	if helper.SupportsFormat(format) { | ||||||
| 			name := strconv.Itoa(userid) + ".png" | 		b.Log.Debugf("Format supported by %s, converting %v", helper.LottieBackend(), name) | ||||||
| 			b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize) | 	} 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) | func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) { | ||||||
| 			if err != nil { | 	if b.GetBool("MediaConvertWebPToPNG") { | ||||||
| 				b.Log.Error(err) | 		b.Log.Debugf("WebP to PNG conversion enabled, converting %v", name) | ||||||
| 				return | 		err := helper.ConvertWebPToPNG(data) | ||||||
| 			} | 		if err != nil { | ||||||
| 			data, err := helper.DownloadFile(url) | 			b.Log.Errorf("conversion failed: %v", err) | ||||||
| 			if err != nil { | 		} else { | ||||||
| 				b.Log.Errorf("download %s failed %#v", url, err) | 			*name = strings.Replace(*name, ".webp", ".png", 1) | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General) |  | ||||||
| 			b.Remote <- rmsg |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -230,7 +311,7 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa | |||||||
| 		name = message.Document.FileName | 		name = message.Document.FileName | ||||||
| 		text = " " + message.Document.FileName + " : " + url | 		text = " " + message.Document.FileName + " : " + url | ||||||
| 	case message.Photo != nil: | 	case message.Photo != nil: | ||||||
| 		photos := *message.Photo | 		photos := message.Photo | ||||||
| 		size = photos[len(photos)-1].FileSize | 		size = photos[len(photos)-1].FileSize | ||||||
| 		text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true) | 		text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true) | ||||||
| 	} | 	} | ||||||
| @@ -254,15 +335,18 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if strings.HasSuffix(name, ".webp") && b.GetBool("MediaConvertWebPToPNG") { |  | ||||||
| 		b.Log.Debugf("WebP to PNG conversion enabled, converting %s", name) | 	if strings.HasSuffix(name, ".tgs.webp") { | ||||||
| 		err := helper.ConvertWebPToPNG(data) | 		b.maybeConvertTgs(&name, data) | ||||||
| 		if err != nil { | 	} else if strings.HasSuffix(name, ".webp") { | ||||||
| 			b.Log.Errorf("conversion failed: %s", err) | 		b.maybeConvertWebp(&name, data) | ||||||
| 		} else { |  | ||||||
| 			name = strings.Replace(name, ".webp", ".png", 1) |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// rename .oga to .ogg  https://github.com/42wim/matterbridge/issues/906#issuecomment-741793512 | ||||||
|  | 	if strings.HasSuffix(name, ".oga") && message.Audio != nil { | ||||||
|  | 		name = strings.Replace(name, ".oga", ".ogg", 1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General) | 	helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @@ -286,11 +370,15 @@ func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, err | |||||||
| 	if msg.ID == "" { | 	if msg.ID == "" { | ||||||
| 		return "", nil | 		return "", nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	msgid, err := strconv.Atoi(msg.ID) | 	msgid, err := strconv.Atoi(msg.ID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 	_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid}) |  | ||||||
|  | 	cfg := tgbotapi.NewDeleteMessage(chatid, msgid) | ||||||
|  | 	_, err = b.c.Send(cfg) | ||||||
|  |  | ||||||
| 	return "", err | 	return "", err | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -312,6 +400,9 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error | |||||||
| 	case "Markdown": | 	case "Markdown": | ||||||
| 		b.Log.Debug("Using mode markdown") | 		b.Log.Debug("Using mode markdown") | ||||||
| 		m.ParseMode = tgbotapi.ModeMarkdown | 		m.ParseMode = tgbotapi.ModeMarkdown | ||||||
|  | 	case MarkdownV2: | ||||||
|  | 		b.Log.Debug("Using mode MarkdownV2") | ||||||
|  | 		m.ParseMode = MarkdownV2 | ||||||
| 	} | 	} | ||||||
| 	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { | 	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { | ||||||
| 		b.Log.Debug("Using mode HTML - nick only") | 		b.Log.Debug("Using mode HTML - nick only") | ||||||
| @@ -333,21 +424,32 @@ func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string { | |||||||
| 			Name:  fi.Name, | 			Name:  fi.Name, | ||||||
| 			Bytes: *fi.Data, | 			Bytes: *fi.Data, | ||||||
| 		} | 		} | ||||||
| 		re := regexp.MustCompile(".(jpg|png)$") | 		switch filepath.Ext(fi.Name) { | ||||||
| 		if re.MatchString(fi.Name) { | 		case ".jpg", ".jpe", ".png": | ||||||
| 			c = tgbotapi.NewPhotoUpload(chatid, file) | 			pc := tgbotapi.NewPhoto(chatid, file) | ||||||
| 		} else { | 			pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) | ||||||
| 			c = tgbotapi.NewDocumentUpload(chatid, file) | 			c = pc | ||||||
|  | 		case ".mp4", ".m4v": | ||||||
|  | 			vc := tgbotapi.NewVideo(chatid, file) | ||||||
|  | 			vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) | ||||||
|  | 			c = vc | ||||||
|  | 		case ".mp3", ".oga": | ||||||
|  | 			ac := tgbotapi.NewAudio(chatid, file) | ||||||
|  | 			ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) | ||||||
|  | 			c = ac | ||||||
|  | 		case ".ogg": | ||||||
|  | 			voc := tgbotapi.NewVoice(chatid, file) | ||||||
|  | 			voc.Caption, voc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) | ||||||
|  | 			c = voc | ||||||
|  | 		default: | ||||||
|  | 			dc := tgbotapi.NewDocument(chatid, file) | ||||||
|  | 			dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) | ||||||
|  | 			c = dc | ||||||
| 		} | 		} | ||||||
| 		_, err := b.c.Send(c) | 		_, err := b.c.Send(c) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			b.Log.Errorf("file upload failed: %#v", err) | 			b.Log.Errorf("file upload failed: %#v", err) | ||||||
| 		} | 		} | ||||||
| 		if fi.Comment != "" { |  | ||||||
| 			if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil { |  | ||||||
| 				b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 	return "" | 	return "" | ||||||
| } | } | ||||||
| @@ -357,6 +459,14 @@ func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string | |||||||
| 	if format == "" { | 	if format == "" { | ||||||
| 		format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" | 		format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" | ||||||
| 	} | 	} | ||||||
|  | 	quoteMessagelength := len([]rune(quoteMessage)) | ||||||
|  | 	if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") { | ||||||
|  | 		runes := []rune(quoteMessage) | ||||||
|  | 		quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")]) | ||||||
|  | 		if quoteMessagelength > b.GetInt("QuoteLengthLimit") { | ||||||
|  | 			quoteMessage += "..." | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	format = strings.Replace(format, "{MESSAGE}", message, -1) | 	format = strings.Replace(format, "{MESSAGE}", message, -1) | ||||||
| 	format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1) | 	format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1) | ||||||
| 	format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1) | 	format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1) | ||||||
| @@ -368,8 +478,11 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa | |||||||
| 	if message.Entities == nil { | 	if message.Entities == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	indexMovedBy := 0 | ||||||
|  |  | ||||||
| 	// for now only do URL replacements | 	// for now only do URL replacements | ||||||
| 	for _, e := range *message.Entities { | 	for _, e := range message.Entities { | ||||||
| 		if e.Type == "text_link" { | 		if e.Type == "text_link" { | ||||||
| 			url, err := e.ParseURL() | 			url, err := e.ParseURL() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @@ -384,5 +497,17 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa | |||||||
| 			link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length]) | 			link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length]) | ||||||
| 			rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1) | 			rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if e.Type == "code" { | ||||||
|  | 			offset := e.Offset + indexMovedBy | ||||||
|  | 			rmsg.Text = rmsg.Text[:offset] + "`" + rmsg.Text[offset:offset+e.Length] + "`" + rmsg.Text[offset+e.Length:] | ||||||
|  | 			indexMovedBy += 2 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if e.Type == "pre" { | ||||||
|  | 			offset := e.Offset + indexMovedBy | ||||||
|  | 			rmsg.Text = rmsg.Text[:offset] + "```\n" + rmsg.Text[offset:offset+e.Length] + "\n```" + rmsg.Text[offset+e.Length:] | ||||||
|  | 			indexMovedBy += 8 | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,19 +2,21 @@ package btelegram | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"html" | 	"html" | ||||||
|  | 	"log" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/go-telegram-bot-api/telegram-bot-api" | 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	unknownUser = "unknown" | 	unknownUser = "unknown" | ||||||
| 	HTMLFormat  = "HTML" | 	HTMLFormat  = "HTML" | ||||||
| 	HTMLNick    = "htmlnick" | 	HTMLNick    = "htmlnick" | ||||||
|  | 	MarkdownV2  = "MarkdownV2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Btelegram struct { | type Btelegram struct { | ||||||
| @@ -24,6 +26,16 @@ type Btelegram struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
|  | 	tgsConvertFormat := cfg.GetString("MediaConvertTgs") | ||||||
|  | 	if tgsConvertFormat != "" { | ||||||
|  | 		err := helper.CanConvertTgsToX() | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s does not appear to work:\n%#v", tgsConvertFormat, helper.LottieBackend(), err) | ||||||
|  | 		} | ||||||
|  | 		if !helper.SupportsFormat(tgsConvertFormat) { | ||||||
|  | 			log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s doesn't support it.", tgsConvertFormat, helper.LottieBackend()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	return &Btelegram{Config: cfg, avatarMap: make(map[string]string)} | 	return &Btelegram{Config: cfg, avatarMap: make(map[string]string)} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -37,11 +49,7 @@ func (b *Btelegram) Connect() error { | |||||||
| 	} | 	} | ||||||
| 	u := tgbotapi.NewUpdate(0) | 	u := tgbotapi.NewUpdate(0) | ||||||
| 	u.Timeout = 60 | 	u.Timeout = 60 | ||||||
| 	updates, err := b.c.GetUpdatesChan(u) | 	updates := b.c.GetUpdatesChan(u) | ||||||
| 	if err != nil { |  | ||||||
| 		b.Log.Debugf("%#v", err) |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	b.Log.Info("Connection succeeded") | 	b.Log.Info("Connection succeeded") | ||||||
| 	go b.handleRecv(updates) | 	go b.handleRecv(updates) | ||||||
| 	return nil | 	return nil | ||||||
| @@ -55,6 +63,28 @@ func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TGGetParseMode(b *Btelegram, username string, text string) (textout string, parsemode string) { | ||||||
|  | 	textout = username + text | ||||||
|  | 	if b.GetString("MessageFormat") == HTMLFormat { | ||||||
|  | 		b.Log.Debug("Using mode HTML") | ||||||
|  | 		parsemode = tgbotapi.ModeHTML | ||||||
|  | 	} | ||||||
|  | 	if b.GetString("MessageFormat") == "Markdown" { | ||||||
|  | 		b.Log.Debug("Using mode markdown") | ||||||
|  | 		parsemode = tgbotapi.ModeMarkdown | ||||||
|  | 	} | ||||||
|  | 	if b.GetString("MessageFormat") == MarkdownV2 { | ||||||
|  | 		b.Log.Debug("Using mode MarkdownV2") | ||||||
|  | 		parsemode = MarkdownV2 | ||||||
|  | 	} | ||||||
|  | 	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { | ||||||
|  | 		b.Log.Debug("Using mode HTML - nick only") | ||||||
|  | 		textout = username + html.EscapeString(text) | ||||||
|  | 		parsemode = tgbotapi.ModeHTML | ||||||
|  | 	} | ||||||
|  | 	return textout, parsemode | ||||||
|  | } | ||||||
|  |  | ||||||
| func (b *Btelegram) Send(msg config.Message) (string, error) { | func (b *Btelegram) Send(msg config.Message) (string, error) { | ||||||
| 	b.Log.Debugf("=> Receiving %#v", msg) | 	b.Log.Debugf("=> Receiving %#v", msg) | ||||||
|  |  | ||||||
| @@ -81,8 +111,8 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { | |||||||
| 	// Upload a file if it exists | 	// Upload a file if it exists | ||||||
| 	if msg.Extra != nil { | 	if msg.Extra != nil { | ||||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | ||||||
| 			if _, err := b.sendMessage(chatid, rmsg.Username, rmsg.Text); err != nil { | 			if _, msgErr := b.sendMessage(chatid, rmsg.Username, rmsg.Text); msgErr != nil { | ||||||
| 				b.Log.Errorf("sendMessage failed: %s", err) | 				b.Log.Errorf("sendMessage failed: %s", msgErr) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		// check if we have files to upload (from slack, telegram or mattermost) | 		// check if we have files to upload (from slack, telegram or mattermost) | ||||||
| @@ -97,7 +127,14 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Post normal message | 	// Post normal message | ||||||
| 	return b.sendMessage(chatid, msg.Username, msg.Text) | 	// TODO: recheck it. | ||||||
|  | 	// Ignore empty text field needs for prevent double messages from whatsapp to telegram | ||||||
|  | 	// when sending media with text caption | ||||||
|  | 	if msg.Text != "" { | ||||||
|  | 		return b.sendMessage(chatid, msg.Username, msg.Text) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "", nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Btelegram) getFileDirectURL(id string) string { | func (b *Btelegram) getFileDirectURL(id string) string { | ||||||
| @@ -110,20 +147,10 @@ func (b *Btelegram) getFileDirectURL(id string) string { | |||||||
|  |  | ||||||
| func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) { | func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) { | ||||||
| 	m := tgbotapi.NewMessage(chatid, "") | 	m := tgbotapi.NewMessage(chatid, "") | ||||||
| 	m.Text = username + text | 	m.Text, m.ParseMode = TGGetParseMode(b, username, text) | ||||||
| 	if b.GetString("MessageFormat") == HTMLFormat { |  | ||||||
| 		b.Log.Debug("Using mode HTML") | 	m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview") | ||||||
| 		m.ParseMode = tgbotapi.ModeHTML |  | ||||||
| 	} |  | ||||||
| 	if b.GetString("MessageFormat") == "Markdown" { |  | ||||||
| 		b.Log.Debug("Using mode markdown") |  | ||||||
| 		m.ParseMode = tgbotapi.ModeMarkdown |  | ||||||
| 	} |  | ||||||
| 	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { |  | ||||||
| 		b.Log.Debug("Using mode HTML - nick only") |  | ||||||
| 		m.Text = username + html.EscapeString(text) |  | ||||||
| 		m.ParseMode = tgbotapi.ModeHTML |  | ||||||
| 	} |  | ||||||
| 	res, err := b.c.Send(m) | 	res, err := b.c.Send(m) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
|   | |||||||
							
								
								
									
										332
									
								
								bridge/vk/vk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										332
									
								
								bridge/vk/vk.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,332 @@ | |||||||
|  | package bvk | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"context" | ||||||
|  | 	"regexp" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/42wim/matterbridge/bridge" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
|  |  | ||||||
|  | 	"github.com/SevereCloud/vksdk/v2/api" | ||||||
|  | 	"github.com/SevereCloud/vksdk/v2/events" | ||||||
|  | 	longpoll "github.com/SevereCloud/vksdk/v2/longpoll-bot" | ||||||
|  | 	"github.com/SevereCloud/vksdk/v2/object" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	audioMessage = "audio_message" | ||||||
|  | 	document     = "doc" | ||||||
|  | 	photo        = "photo" | ||||||
|  | 	video        = "video" | ||||||
|  | 	graffiti     = "graffiti" | ||||||
|  | 	sticker      = "sticker" | ||||||
|  | 	wall         = "wall" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type user struct { | ||||||
|  | 	lastname, firstname, avatar string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Bvk struct { | ||||||
|  | 	c            *api.VK | ||||||
|  | 	lp           *longpoll.LongPoll | ||||||
|  | 	usernamesMap map[int]user // cache of user names and avatar URLs | ||||||
|  | 	*bridge.Config | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
|  | 	return &Bvk{usernamesMap: make(map[int]user), Config: cfg} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bvk) Connect() error { | ||||||
|  | 	b.Log.Info("Connecting") | ||||||
|  | 	b.c = api.NewVK(b.GetString("Token")) | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
|  | 	b.lp, err = longpoll.NewLongPollCommunity(b.c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Debugf("%#v", err) | ||||||
|  |  | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) { | ||||||
|  | 		b.handleMessage(obj.Message, false) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	b.Log.Info("Connection succeeded") | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		err := b.lp.Run() | ||||||
|  | 		if err != nil { | ||||||
|  | 			b.Log.Fatal("Enable longpoll in group management") | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bvk) Disconnect() error { | ||||||
|  | 	b.lp.Shutdown() | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bvk) JoinChannel(channel config.ChannelInfo) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bvk) Send(msg config.Message) (string, error) { | ||||||
|  | 	b.Log.Debugf("=> Receiving %#v", msg) | ||||||
|  |  | ||||||
|  | 	peerID, err := strconv.Atoi(msg.Channel) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	params := api.Params{} | ||||||
|  |  | ||||||
|  | 	text := msg.Username + msg.Text | ||||||
|  |  | ||||||
|  | 	if msg.Extra != nil { | ||||||
|  | 		if len(msg.Extra["file"]) > 0 { | ||||||
|  | 			// generate attachments string | ||||||
|  | 			attachment, urls := b.uploadFiles(msg.Extra, peerID) | ||||||
|  | 			params["attachment"] = attachment | ||||||
|  | 			text += urls | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	params["message"] = text | ||||||
|  |  | ||||||
|  | 	if msg.ID == "" { | ||||||
|  | 		// New message | ||||||
|  | 		params["random_id"] = time.Now().Unix() | ||||||
|  | 		params["peer_ids"] = msg.Channel | ||||||
|  |  | ||||||
|  | 		res, e := b.c.MessagesSendPeerIDs(params) | ||||||
|  | 		if e != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return strconv.Itoa(res[0].ConversationMessageID), nil | ||||||
|  | 	} | ||||||
|  | 	// Edit message | ||||||
|  | 	messageID, err := strconv.ParseInt(msg.ID, 10, 64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	params["peer_id"] = peerID | ||||||
|  | 	params["conversation_message_id"] = messageID | ||||||
|  |  | ||||||
|  | 	_, err = b.c.MessagesEdit(params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return msg.ID, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bvk) getUser(id int) user { | ||||||
|  | 	u, found := b.usernamesMap[id] | ||||||
|  | 	if !found { | ||||||
|  | 		b.Log.Debug("Fetching username for ", id) | ||||||
|  |  | ||||||
|  | 		if id >= 0 { | ||||||
|  | 			result, _ := b.c.UsersGet(api.Params{ | ||||||
|  | 				"user_ids": id, | ||||||
|  | 				"fields":   "photo_200", | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			resUser := result[0] | ||||||
|  | 			u = user{lastname: resUser.LastName, firstname: resUser.FirstName, avatar: resUser.Photo200} | ||||||
|  | 			b.usernamesMap[id] = u | ||||||
|  | 		} else { | ||||||
|  | 			result, _ := b.c.GroupsGetByID(api.Params{ | ||||||
|  | 				"group_id": id * -1, | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			resGroup := result[0] | ||||||
|  | 			u = user{lastname: resGroup.Name, avatar: resGroup.Photo200} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return u | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bvk) handleMessage(msg object.MessagesMessage, isFwd bool) { | ||||||
|  | 	b.Log.Debug("ChatID: ", msg.PeerID) | ||||||
|  | 	// fetch user info | ||||||
|  | 	u := b.getUser(msg.FromID) | ||||||
|  |  | ||||||
|  | 	rmsg := config.Message{ | ||||||
|  | 		Text:     msg.Text, | ||||||
|  | 		Username: u.firstname + " " + u.lastname, | ||||||
|  | 		Avatar:   u.avatar, | ||||||
|  | 		Channel:  strconv.Itoa(msg.PeerID), | ||||||
|  | 		Account:  b.Account, | ||||||
|  | 		UserID:   strconv.Itoa(msg.FromID), | ||||||
|  | 		ID:       strconv.Itoa(msg.ConversationMessageID), | ||||||
|  | 		Extra:    make(map[string][]interface{}), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if msg.ReplyMessage != nil { | ||||||
|  | 		ur := b.getUser(msg.ReplyMessage.FromID) | ||||||
|  | 		rmsg.Text = "Re: " + ur.firstname + " " + ur.lastname + "\n" + rmsg.Text | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if isFwd { | ||||||
|  | 		rmsg.Username = "Fwd: " + rmsg.Username | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(msg.Attachments) > 0 { | ||||||
|  | 		urls, text := b.getFiles(msg.Attachments) | ||||||
|  |  | ||||||
|  | 		if text != "" { | ||||||
|  | 			rmsg.Text += "\n" + text | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// download | ||||||
|  | 		b.downloadFiles(&rmsg, urls) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(msg.FwdMessages) > 0 { | ||||||
|  | 		rmsg.Text += strconv.Itoa(len(msg.FwdMessages)) + " forwarded messages" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.Remote <- rmsg | ||||||
|  |  | ||||||
|  | 	if len(msg.FwdMessages) > 0 { | ||||||
|  | 		// recursive processing of forwarded messages | ||||||
|  | 		for _, m := range msg.FwdMessages { | ||||||
|  | 			m.PeerID = msg.PeerID | ||||||
|  | 			b.handleMessage(m, true) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, string) { | ||||||
|  | 	var attachments []string | ||||||
|  | 	text := "" | ||||||
|  |  | ||||||
|  | 	for _, f := range extra["file"] { | ||||||
|  | 		fi := f.(config.FileInfo) | ||||||
|  |  | ||||||
|  | 		if fi.Comment != "" { | ||||||
|  | 			text += fi.Comment + "\n" | ||||||
|  | 		} | ||||||
|  | 		a, err := b.uploadFile(fi, peerID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			b.Log.Error("File upload error ", fi.Name) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		attachments = append(attachments, a) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return strings.Join(attachments, ","), text | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) { | ||||||
|  | 	r := bytes.NewReader(*file.Data) | ||||||
|  |  | ||||||
|  | 	photoRE := regexp.MustCompile(".(jpg|jpe|png)$") | ||||||
|  | 	if photoRE.MatchString(file.Name) { | ||||||
|  | 		p, err := b.c.UploadMessagesPhoto(peerID, r) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return photo + strconv.Itoa(p[0].OwnerID) + "_" + strconv.Itoa(p[0].ID), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var doctype string | ||||||
|  | 	if strings.Contains(file.Name, ".ogg") { | ||||||
|  | 		doctype = audioMessage | ||||||
|  | 	} else { | ||||||
|  | 		doctype = document | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	doc, err := b.c.UploadMessagesDoc(peerID, doctype, file.Name, "", r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch doc.Type { | ||||||
|  | 	case audioMessage: | ||||||
|  | 		return document + strconv.Itoa(doc.AudioMessage.OwnerID) + "_" + strconv.Itoa(doc.AudioMessage.ID), nil | ||||||
|  | 	case document: | ||||||
|  | 		return document + strconv.Itoa(doc.Doc.OwnerID) + "_" + strconv.Itoa(doc.Doc.ID), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "", nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bvk) getFiles(attachments []object.MessagesMessageAttachment) ([]string, string) { | ||||||
|  | 	var urls []string | ||||||
|  | 	var text []string | ||||||
|  |  | ||||||
|  | 	for _, a := range attachments { | ||||||
|  | 		switch a.Type { | ||||||
|  | 		case photo: | ||||||
|  | 			var resolution float64 = 0 | ||||||
|  | 			url := a.Photo.Sizes[0].URL | ||||||
|  | 			for _, size := range a.Photo.Sizes { | ||||||
|  | 				r := size.Height * size.Width | ||||||
|  | 				if resolution < r { | ||||||
|  | 					resolution = r | ||||||
|  | 					url = size.URL | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			urls = append(urls, url) | ||||||
|  |  | ||||||
|  | 		case document: | ||||||
|  | 			urls = append(urls, a.Doc.URL) | ||||||
|  |  | ||||||
|  | 		case graffiti: | ||||||
|  | 			urls = append(urls, a.Graffiti.URL) | ||||||
|  |  | ||||||
|  | 		case audioMessage: | ||||||
|  | 			urls = append(urls, a.AudioMessage.DocsDocPreviewAudioMessage.LinkOgg) | ||||||
|  |  | ||||||
|  | 		case sticker: | ||||||
|  | 			var resolution float64 = 0 | ||||||
|  | 			url := a.Sticker.Images[0].URL | ||||||
|  | 			for _, size := range a.Sticker.Images { | ||||||
|  | 				r := size.Height * size.Width | ||||||
|  | 				if resolution < r { | ||||||
|  | 					resolution = r | ||||||
|  | 					url = size.URL | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			urls = append(urls, url+".png") | ||||||
|  | 		case video: | ||||||
|  | 			text = append(text, "https://vk.com/video"+strconv.Itoa(a.Video.OwnerID)+"_"+strconv.Itoa(a.Video.ID)) | ||||||
|  |  | ||||||
|  | 		case wall: | ||||||
|  | 			text = append(text, "https://vk.com/wall"+strconv.Itoa(a.Wall.FromID)+"_"+strconv.Itoa(a.Wall.ID)) | ||||||
|  |  | ||||||
|  | 		default: | ||||||
|  | 			text = append(text, "This attachment is not supported ("+a.Type+")") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return urls, strings.Join(text, "\n") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bvk) downloadFiles(rmsg *config.Message, urls []string) { | ||||||
|  | 	for _, url := range urls { | ||||||
|  | 		data, err := helper.DownloadFile(url) | ||||||
|  | 		if err == nil { | ||||||
|  | 			urlPart := strings.Split(url, "/") | ||||||
|  | 			name := strings.Split(urlPart[len(urlPart)-1], "?")[0] | ||||||
|  | 			helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,11 +1,15 @@ | |||||||
| package bwhatsapp | package bwhatsapp | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"mime" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
| 	"github.com/Rhymen/go-whatsapp" | 	"github.com/Rhymen/go-whatsapp" | ||||||
|  | 	"github.com/jpillora/backoff" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| /* | /* | ||||||
| @@ -19,15 +23,55 @@ Check: | |||||||
| // HandleError received from WhatsApp | // HandleError received from WhatsApp | ||||||
| func (b *Bwhatsapp) HandleError(err error) { | func (b *Bwhatsapp) HandleError(err error) { | ||||||
| 	// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843 | 	// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843 | ||||||
| 	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 | 		return | ||||||
| 	} | 	} | ||||||
| 	b.Log.Errorf("%v", err) // TODO implement proper handling? at least respond to different error types |  | ||||||
|  | 	switch err.(type) { | ||||||
|  | 	case *whatsapp.ErrConnectionClosed, *whatsapp.ErrConnectionFailed: | ||||||
|  | 		b.reconnect(err) | ||||||
|  | 	default: | ||||||
|  | 		switch err { | ||||||
|  | 		case whatsapp.ErrConnectionTimeout: | ||||||
|  | 			b.reconnect(err) | ||||||
|  | 		default: | ||||||
|  | 			b.Log.Errorf("%v", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bwhatsapp) reconnect(err error) { | ||||||
|  | 	bf := &backoff.Backoff{ | ||||||
|  | 		Min:    time.Second, | ||||||
|  | 		Max:    5 * time.Minute, | ||||||
|  | 		Jitter: true, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		d := bf.Duration() | ||||||
|  |  | ||||||
|  | 		b.Log.Errorf("Connection failed, underlying error: %v", err) | ||||||
|  | 		b.Log.Infof("Waiting %s...", d) | ||||||
|  |  | ||||||
|  | 		time.Sleep(d) | ||||||
|  |  | ||||||
|  | 		b.Log.Info("Reconnecting...") | ||||||
|  |  | ||||||
|  | 		err := b.conn.Restore() | ||||||
|  | 		if err == nil { | ||||||
|  | 			bf.Reset() | ||||||
|  | 			b.startedAt = uint64(time.Now().Unix()) | ||||||
|  |  | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // HandleTextMessage sent from WhatsApp, relay it to the brige | // HandleTextMessage sent from WhatsApp, relay it to the brige | ||||||
| func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { | func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { | ||||||
| 	if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") { | 	if message.Info.FromMe { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	// whatsapp sends last messages to show context , cut them | 	// whatsapp sends last messages to show context , cut them | ||||||
| @@ -35,17 +79,17 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones | 	groupJID := message.Info.RemoteJid | ||||||
| 	groupJid := message.Info.RemoteJid | 	senderJID := message.Info.SenderJid | ||||||
|  |  | ||||||
| 	senderJid := message.Info.SenderJid | 	if len(senderJID) == 0 { | ||||||
| 	if len(senderJid) == 0 { | 		if message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||||
| 		// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved | 			senderJID = *message.Info.Source.Participant | ||||||
| 		senderJid = *message.Info.Source.Participant | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// translate sender's Jid to the nicest username we can get | 	// translate sender's JID to the nicest username we can get | ||||||
| 	senderName := b.getSenderName(senderJid) | 	senderName := b.getSenderName(senderJID) | ||||||
| 	if senderName == "" { | 	if senderName == "" { | ||||||
| 		senderName = "Someone" // don't expose telephone number | 		senderName = "Someone" // don't expose telephone number | ||||||
| 	} | 	} | ||||||
| @@ -53,8 +97,8 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { | |||||||
| 	extText := message.Info.Source.Message.ExtendedTextMessage | 	extText := message.Info.Source.Message.ExtendedTextMessage | ||||||
| 	if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil { | 	if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil { | ||||||
| 		// handle user mentions | 		// handle user mentions | ||||||
| 		for _, mentionedJid := range extText.ContextInfo.MentionedJid { | 		for _, mentionedJID := range extText.ContextInfo.MentionedJid { | ||||||
| 			numberAndSuffix := strings.SplitN(mentionedJid, "@", 2) | 			numberAndSuffix := strings.SplitN(mentionedJID, "@", 2) | ||||||
|  |  | ||||||
| 			// mentions comes as telephone numbers and we don't want to expose it to other bridges | 			// mentions comes as telephone numbers and we don't want to expose it to other bridges | ||||||
| 			// replace it with something more meaninful to others | 			// replace it with something more meaninful to others | ||||||
| @@ -62,44 +106,275 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { | |||||||
| 			if mention == "" { | 			if mention == "" { | ||||||
| 				mention = "someone" | 				mention = "someone" | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1) | 			message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJid, b.Account) |  | ||||||
| 	rmsg := config.Message{ | 	rmsg := config.Message{ | ||||||
| 		UserID:    senderJid, | 		UserID:   senderJID, | ||||||
| 		Username:  senderName, | 		Username: senderName, | ||||||
| 		Text:      message.Text, | 		Text:     message.Text, | ||||||
| 		Timestamp: messageTime, | 		Channel:  groupJID, | ||||||
| 		Channel:   groupJid, | 		Account:  b.Account, | ||||||
| 		Account:   b.Account, | 		Protocol: b.Protocol, | ||||||
| 		Protocol:  b.Protocol, | 		Extra:    make(map[string][]interface{}), | ||||||
| 		Extra:     make(map[string][]interface{}), | 		//	ParentID: TODO, // TODO handle thread replies  // map from Info.QuotedMessageID string | ||||||
| 		//		ParentID: TODO, // TODO handle thread replies  // map from Info.QuotedMessageID string | 		ID: message.Info.Id, | ||||||
| 		//	Event     string    `json:"event"` | 	} | ||||||
| 		//	Gateway   string  // will be added during message processing |  | ||||||
| 		ID: message.Info.Id} |  | ||||||
|  |  | ||||||
| 	if avatarURL, exists := b.userAvatars[senderJid]; exists { | 	if avatarURL, exists := b.userAvatars[senderJID]; exists { | ||||||
| 		rmsg.Avatar = avatarURL | 		rmsg.Avatar = avatarURL | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||||
| 	b.Log.Debugf("<= Message is %#v", rmsg) | 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
|  |  | ||||||
| 	b.Remote <- rmsg | 	b.Remote <- rmsg | ||||||
| } | } | ||||||
|  |  | ||||||
| // | // HandleImageMessage sent from WhatsApp, relay it to the brige | ||||||
| //func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) { | func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) { | ||||||
| //	fmt.Println(message) // TODO implement | 	if message.Info.FromMe || message.Info.Timestamp < b.startedAt { | ||||||
| //} | 		return | ||||||
| // | 	} | ||||||
| //func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) { |  | ||||||
| //	fmt.Println(message) // TODO implement | 	senderJID := message.Info.SenderJid | ||||||
| //} | 	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||||
| // | 		senderJID = *message.Info.Source.Participant | ||||||
| //func (b *Bwhatsapp) HandleJsonMessage(message string) { | 	} | ||||||
| //	fmt.Println(message) // TODO implement |  | ||||||
| //} | 	senderName := b.getSenderName(message.Info.SenderJid) | ||||||
| // TODO HandleRawMessage | 	if senderName == "" { | ||||||
| // TODO HandleAudioMessage | 		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 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292 | ||||||
|  | 	if fileExt[0] == ".jfif" { | ||||||
|  | 		fileExt[0] = ".jpg" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463 | ||||||
|  | 	if fileExt[0] == ".jpe" { | ||||||
|  | 		fileExt[0] = ".jpg" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0]) | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("Trying to download %s with type %s", filename, message.Type) | ||||||
|  |  | ||||||
|  | 	data, err := message.Download() | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Download image failed: %s", err) | ||||||
|  |  | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Move file to bridge storage | ||||||
|  | 	helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General) | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||||
|  | 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
|  |  | ||||||
|  | 	b.Remote <- rmsg | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HandleVideoMessage downloads video messages | ||||||
|  | func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) { | ||||||
|  | 	if message.Info.FromMe || message.Info.Timestamp < b.startedAt { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	senderJID := message.Info.SenderJid | ||||||
|  | 	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||||
|  | 		senderJID = *message.Info.Source.Participant | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	senderName := b.getSenderName(message.Info.SenderJid) | ||||||
|  | 	if senderName == "" { | ||||||
|  | 		senderName = "Someone" // don't expose telephone number | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rmsg := config.Message{ | ||||||
|  | 		UserID:   senderJID, | ||||||
|  | 		Username: senderName, | ||||||
|  | 		Channel:  message.Info.RemoteJid, | ||||||
|  | 		Account:  b.Account, | ||||||
|  | 		Protocol: b.Protocol, | ||||||
|  | 		Extra:    make(map[string][]interface{}), | ||||||
|  | 		ID:       message.Info.Id, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if avatarURL, exists := b.userAvatars[senderJID]; exists { | ||||||
|  | 		rmsg.Avatar = avatarURL | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fileExt, err := mime.ExtensionsByType(message.Type) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||||
|  |  | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(fileExt) == 0 { | ||||||
|  | 		fileExt = append(fileExt, ".mp4") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0]) | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type) | ||||||
|  |  | ||||||
|  | 	data, err := message.Download() | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Download video failed: %s", err) | ||||||
|  |  | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Move file to bridge storage | ||||||
|  | 	helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General) | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||||
|  | 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
|  |  | ||||||
|  | 	b.Remote <- rmsg | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HandleAudioMessage downloads audio messages | ||||||
|  | func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) { | ||||||
|  | 	if message.Info.FromMe || message.Info.Timestamp < b.startedAt { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	senderJID := message.Info.SenderJid | ||||||
|  | 	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||||
|  | 		senderJID = *message.Info.Source.Participant | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	senderName := b.getSenderName(message.Info.SenderJid) | ||||||
|  | 	if senderName == "" { | ||||||
|  | 		senderName = "Someone" // don't expose telephone number | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rmsg := config.Message{ | ||||||
|  | 		UserID:   senderJID, | ||||||
|  | 		Username: senderName, | ||||||
|  | 		Channel:  message.Info.RemoteJid, | ||||||
|  | 		Account:  b.Account, | ||||||
|  | 		Protocol: b.Protocol, | ||||||
|  | 		Extra:    make(map[string][]interface{}), | ||||||
|  | 		ID:       message.Info.Id, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if avatarURL, exists := b.userAvatars[senderJID]; exists { | ||||||
|  | 		rmsg.Avatar = avatarURL | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fileExt, err := mime.ExtensionsByType(message.Type) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||||
|  |  | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(fileExt) == 0 { | ||||||
|  | 		fileExt = append(fileExt, ".ogg") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0]) | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type) | ||||||
|  |  | ||||||
|  | 	data, err := message.Download() | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Download audio failed: %s", err) | ||||||
|  |  | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Move file to bridge storage | ||||||
|  | 	helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General) | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||||
|  | 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
|  |  | ||||||
|  | 	b.Remote <- rmsg | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HandleDocumentMessage downloads documents | ||||||
|  | func (b *Bwhatsapp) HandleDocumentMessage(message whatsapp.DocumentMessage) { | ||||||
|  | 	if message.Info.FromMe || message.Info.Timestamp < b.startedAt { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	senderJID := message.Info.SenderJid | ||||||
|  | 	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil { | ||||||
|  | 		senderJID = *message.Info.Source.Participant | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	senderName := b.getSenderName(message.Info.SenderJid) | ||||||
|  | 	if senderName == "" { | ||||||
|  | 		senderName = "Someone" // don't expose telephone number | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rmsg := config.Message{ | ||||||
|  | 		UserID:   senderJID, | ||||||
|  | 		Username: senderName, | ||||||
|  | 		Channel:  message.Info.RemoteJid, | ||||||
|  | 		Account:  b.Account, | ||||||
|  | 		Protocol: b.Protocol, | ||||||
|  | 		Extra:    make(map[string][]interface{}), | ||||||
|  | 		ID:       message.Info.Id, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if avatarURL, exists := b.userAvatars[senderJID]; exists { | ||||||
|  | 		rmsg.Avatar = avatarURL | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fileExt, err := mime.ExtensionsByType(message.Type) | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Mimetype detection error: %s", err) | ||||||
|  |  | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	filename := fmt.Sprintf("%v", message.FileName) | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, message.Type) | ||||||
|  |  | ||||||
|  | 	data, err := message.Download() | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("Download document message failed: %s", err) | ||||||
|  |  | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Move file to bridge storage | ||||||
|  | 	helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General) | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account) | ||||||
|  | 	b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
|  |  | ||||||
|  | 	b.Remote <- rmsg | ||||||
|  | } | ||||||
|   | |||||||
| @@ -6,22 +6,24 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go" | 	qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go" | ||||||
| 	"github.com/Rhymen/go-whatsapp" | 	"github.com/Rhymen/go-whatsapp" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type ProfilePicInfo struct { | type ProfilePicInfo struct { | ||||||
| 	URL string `json:"eurl"` | 	URL    string `json:"eurl"` | ||||||
| 	Tag string `json:"tag"` | 	Tag    string `json:"tag"` | ||||||
|  | 	Status int16  `json:"status"` | ||||||
| 	Status int16 `json:"status"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func qrFromTerminal(invert bool) chan string { | func qrFromTerminal(invert bool) chan string { | ||||||
| 	qr := make(chan string) | 	qr := make(chan string) | ||||||
|  |  | ||||||
| 	go func() { | 	go func() { | ||||||
| 		terminal := qrcodeTerminal.New() | 		terminal := qrcodeTerminal.New() | ||||||
|  |  | ||||||
| 		if invert { | 		if invert { | ||||||
| 			terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium) | 			terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium) | ||||||
| 		} | 		} | ||||||
| @@ -44,13 +46,12 @@ func (b *Bwhatsapp) readSession() (whatsapp.Session, error) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return session, err | 		return session, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	defer file.Close() | 	defer file.Close() | ||||||
|  |  | ||||||
| 	decoder := gob.NewDecoder(file) | 	decoder := gob.NewDecoder(file) | ||||||
| 	err = decoder.Decode(&session) |  | ||||||
| 	if err != nil { | 	return session, decoder.Decode(&session) | ||||||
| 		return session, err |  | ||||||
| 	} |  | ||||||
| 	return session, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bwhatsapp) writeSession(session whatsapp.Session) error { | func (b *Bwhatsapp) writeSession(session whatsapp.Session) error { | ||||||
| @@ -65,11 +66,31 @@ func (b *Bwhatsapp) writeSession(session whatsapp.Session) error { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		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 { | func (b *Bwhatsapp) getSenderName(senderJid string) string { | ||||||
| @@ -80,8 +101,33 @@ func (b *Bwhatsapp) getSenderName(senderJid string) string { | |||||||
| 		// if user is not in phone contacts | 		// if user is not in phone contacts | ||||||
| 		// it is the most obvious scenario unless you sync your phone contacts with some remote updated source | 		// it is the most obvious scenario unless you sync your phone contacts with some remote updated source | ||||||
| 		// users can change it in their WhatsApp settings -> profile -> click on Avatar | 		// users can change it in their WhatsApp settings -> profile -> click on Avatar | ||||||
| 		return sender.Notify | 		if sender.Notify != "" { | ||||||
|  | 			return sender.Notify | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if sender.Short != "" { | ||||||
|  | 			return sender.Short | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// try to reload this contact | ||||||
|  | 	_, err := b.conn.Contacts() | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Log.Errorf("error on update of contacts: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if contact, exists := b.conn.Store.Contacts[senderJid]; exists { | ||||||
|  | 		// Add it to the user map | ||||||
|  | 		b.users[senderJid] = contact | ||||||
|  |  | ||||||
|  | 		if contact.Name != "" { | ||||||
|  | 			return contact.Name | ||||||
|  | 		} | ||||||
|  | 		// if user is not in phone contacts | ||||||
|  | 		// same as above | ||||||
|  | 		return contact.Notify | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return "" | 	return "" | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -89,6 +135,7 @@ func (b *Bwhatsapp) getSenderNotify(senderJid string) string { | |||||||
| 	if sender, exists := b.users[senderJid]; exists { | 	if sender, exists := b.users[senderJid]; exists { | ||||||
| 		return sender.Notify | 		return sender.Notify | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return "" | 	return "" | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -97,11 +144,20 @@ func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("failed to get avatar: %v", err) | 		return nil, fmt.Errorf("failed to get avatar: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	content := <-data | 	content := <-data | ||||||
| 	info := &ProfilePicInfo{} | 	info := &ProfilePicInfo{} | ||||||
|  |  | ||||||
| 	err = json.Unmarshal([]byte(content), info) | 	err = json.Unmarshal([]byte(content), info) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return info, fmt.Errorf("failed to unmarshal avatar info: %v", err) | 		return info, fmt.Errorf("failed to unmarshal avatar info: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return info, nil | 	return info, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func isGroupJid(identifier string) bool { | ||||||
|  | 	return strings.HasSuffix(identifier, "@g.us") || | ||||||
|  | 		strings.HasSuffix(identifier, "@temp") || | ||||||
|  | 		strings.HasSuffix(identifier, "@broadcast") | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,11 +1,14 @@ | |||||||
| package bwhatsapp | package bwhatsapp | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"crypto/rand" | 	"crypto/rand" | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"mime" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @@ -25,7 +28,6 @@ const ( | |||||||
| type Bwhatsapp struct { | type Bwhatsapp struct { | ||||||
| 	*bridge.Config | 	*bridge.Config | ||||||
|  |  | ||||||
| 	// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21 |  | ||||||
| 	session   *whatsapp.Session | 	session   *whatsapp.Session | ||||||
| 	conn      *whatsapp.Conn | 	conn      *whatsapp.Conn | ||||||
| 	startedAt uint64 | 	startedAt uint64 | ||||||
| @@ -37,6 +39,7 @@ type Bwhatsapp struct { | |||||||
| // New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file | // New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
| 	number := cfg.GetString(cfgNumber) | 	number := cfg.GetString(cfgNumber) | ||||||
|  |  | ||||||
| 	if number == "" { | 	if number == "" { | ||||||
| 		cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number") | 		cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number") | ||||||
| 	} | 	} | ||||||
| @@ -47,21 +50,17 @@ func New(cfg *bridge.Config) bridge.Bridger { | |||||||
| 		users:       make(map[string]whatsapp.Contact), | 		users:       make(map[string]whatsapp.Contact), | ||||||
| 		userAvatars: make(map[string]string), | 		userAvatars: make(map[string]string), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return b | 	return b | ||||||
| } | } | ||||||
|  |  | ||||||
| // Connect to WhatsApp. Required implementation of the Bridger interface | // Connect to WhatsApp. Required implementation of the Bridger interface | ||||||
| // https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16 |  | ||||||
| func (b *Bwhatsapp) Connect() error { | func (b *Bwhatsapp) Connect() error { | ||||||
| 	b.RLock() // TODO do we need locking for Whatsapp? |  | ||||||
| 	defer b.RUnlock() |  | ||||||
|  |  | ||||||
| 	number := b.GetString(cfgNumber) | 	number := b.GetString(cfgNumber) | ||||||
| 	if number == "" { | 	if number == "" { | ||||||
| 		return errors.New("WhatsApp's telephone Number need to be configured") | 		return errors.New("whatsapp's telephone number need to be configured") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// https://github.com/Rhymen/go-whatsapp#creating-a-connection |  | ||||||
| 	b.Log.Debugln("Connecting to WhatsApp..") | 	b.Log.Debugln("Connecting to WhatsApp..") | ||||||
| 	conn, err := whatsapp.NewConn(20 * time.Second) | 	conn, err := whatsapp.NewConn(20 * time.Second) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -74,35 +73,18 @@ func (b *Bwhatsapp) Connect() error { | |||||||
| 	b.Log.Debugln("WhatsApp connection successful") | 	b.Log.Debugln("WhatsApp connection successful") | ||||||
|  |  | ||||||
| 	// load existing session in order to keep it between restarts | 	// load existing session in order to keep it between restarts | ||||||
| 	if b.session == nil { | 	b.session, err = b.restoreSession() | ||||||
| 		var session whatsapp.Session | 	if err != nil { | ||||||
| 		session, err = b.readSession() | 		b.Log.Warn(err.Error()) | ||||||
|  |  | ||||||
| 		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()) |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// login to a new session | 	// login to a new session | ||||||
| 	if b.session == nil { | 	if b.session == nil { | ||||||
| 		err = b.Login() | 		if err = b.Login(); err != nil { | ||||||
| 		if err != nil { |  | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.startedAt = uint64(time.Now().Unix()) | 	b.startedAt = uint64(time.Now().Unix()) | ||||||
|  |  | ||||||
| 	_, err = b.conn.Contacts() | 	_, err = b.conn.Contacts() | ||||||
| @@ -110,6 +92,13 @@ func (b *Bwhatsapp) Connect() error { | |||||||
| 		return fmt.Errorf("error on update of contacts: %v", err) | 		return fmt.Errorf("error on update of contacts: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013 | ||||||
|  | 	for len(b.conn.Store.Contacts) == 0 { | ||||||
|  | 		b.conn.Contacts() // nolint:errcheck | ||||||
|  |  | ||||||
|  | 		<-time.After(1 * time.Second) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// map all the users | 	// map all the users | ||||||
| 	for id, contact := range b.conn.Store.Contacts { | 	for id, contact := range b.conn.Store.Contacts { | ||||||
| 		if !isGroupJid(id) && id != "status@broadcast" { | 		if !isGroupJid(id) && id != "status@broadcast" { | ||||||
| @@ -126,12 +115,13 @@ func (b *Bwhatsapp) Connect() error { | |||||||
| 			info, err := b.GetProfilePicThumb(jid) | 			info, err := b.GetProfilePicThumb(jid) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				b.Log.Warnf("Could not get profile photo of %s: %v", jid, err) | 				b.Log.Warnf("Could not get profile photo of %s: %v", jid, err) | ||||||
|  |  | ||||||
| 			} else { | 			} else { | ||||||
| 				// TODO any race conditions here? | 				b.Lock() | ||||||
| 				b.userAvatars[jid] = info.URL | 				b.userAvatars[jid] = info.URL | ||||||
|  | 				b.Unlock() | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		b.Log.Debug("Finished getting avatars..") | 		b.Log.Debug("Finished getting avatars..") | ||||||
| 	}() | 	}() | ||||||
|  |  | ||||||
| @@ -148,8 +138,10 @@ func (b *Bwhatsapp) Login() error { | |||||||
| 	session, err := b.conn.Login(qrChan) | 	session, err := b.conn.Login(qrChan) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		b.Log.Warnln("Failed to log in:", err) | 		b.Log.Warnln("Failed to log in:", err) | ||||||
|  |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.session = &session | 	b.session = &session | ||||||
|  |  | ||||||
| 	b.Log.Infof("Logged into session: %#v", session) | 	b.Log.Infof("Logged into session: %#v", session) | ||||||
| @@ -160,74 +152,122 @@ func (b *Bwhatsapp) Login() error { | |||||||
| 		fmt.Fprintf(os.Stderr, "error saving session: %v\n", err) | 		fmt.Fprintf(os.Stderr, "error saving session: %v\n", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// TODO change connection strings to configured ones longClientName:"github.com/rhymen/go-whatsapp", shortClientName:"go-whatsapp"}" prefix=whatsapp |  | ||||||
| 	// TODO get also a nice logo |  | ||||||
|  |  | ||||||
| 	// TODO notification about unplugged and dead battery |  | ||||||
| 	// conn.Info: Wid, Pushname, Connected, Battery, Plugged |  | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Disconnect is called while reconnecting to the bridge | // Disconnect is called while reconnecting to the bridge | ||||||
| // TODO 42wim Documentation would be helpful on when reconnects happen and what should be done in this function |  | ||||||
| // Required implementation of the Bridger interface | // Required implementation of the Bridger interface | ||||||
| // https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16 |  | ||||||
| func (b *Bwhatsapp) Disconnect() error { | func (b *Bwhatsapp) Disconnect() error { | ||||||
| 	// We could Logout, but that would close the session completely and would require a new QR code scan | 	// We could Logout, but that would close the session completely and would require a new QR code scan | ||||||
| 	// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381 | 	// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func isGroupJid(identifier string) bool { |  | ||||||
| 	return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name' | // JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name' | ||||||
| // Required implementation of the Bridger interface | // Required implementation of the Bridger interface | ||||||
| // https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16 | // https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16 | ||||||
| func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error { | func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error { | ||||||
| 	byJid := isGroupJid(channel.Name) | 	byJid := isGroupJid(channel.Name) | ||||||
|  |  | ||||||
|  | 	// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013 | ||||||
|  | 	for len(b.conn.Store.Contacts) == 0 { | ||||||
|  | 		b.conn.Contacts() // nolint:errcheck | ||||||
|  | 		<-time.After(1 * time.Second) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// verify if we are member of the given group | 	// verify if we are member of the given group | ||||||
| 	if byJid { | 	if byJid { | ||||||
| 		// channel.Name specifies static group jID, not the name | 		// channel.Name specifies static group jID, not the name | ||||||
| 		if _, exists := b.conn.Store.Contacts[channel.Name]; !exists { | 		if _, exists := b.conn.Store.Contacts[channel.Name]; !exists { | ||||||
| 			return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name) | 			return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name) | ||||||
| 		} | 		} | ||||||
| 	} else { |  | ||||||
| 		// 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) { | 		return nil | ||||||
| 		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) |  | ||||||
|  |  | ||||||
| 		case 1: | 	// channel.Name specifies group name that might change, warn about it | ||||||
| 			return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name) | 	var jids []string | ||||||
|  | 	for id, contact := range b.conn.Store.Contacts { | ||||||
| 		default: | 		if isGroupJid(id) && contact.Name == channel.Name { | ||||||
| 			return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids) | 			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 | ||||||
|  | func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) { | ||||||
|  | 	fi := msg.Extra["file"][0].(config.FileInfo) | ||||||
|  |  | ||||||
|  | 	// Post document message | ||||||
|  | 	message := whatsapp.DocumentMessage{ | ||||||
|  | 		Info: whatsapp.MessageInfo{ | ||||||
|  | 			RemoteJid: msg.Channel, | ||||||
|  | 		}, | ||||||
|  | 		Title:    fi.Name, | ||||||
|  | 		FileName: fi.Name, | ||||||
|  | 		Type:     filetype, | ||||||
|  | 		Content:  bytes.NewReader(*fi.Data), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("=> Sending %#v", msg) | ||||||
|  |  | ||||||
|  | 	// create message ID | ||||||
|  | 	// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented | ||||||
|  | 	idBytes := make([]byte, 10) | ||||||
|  | 	if _, err := rand.Read(idBytes); err != nil { | ||||||
|  | 		b.Log.Warn(err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes)) | ||||||
|  | 	_, err := b.conn.Send(message) | ||||||
|  |  | ||||||
|  | 	return message.Info.Id, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Post an image message from the bridge to WhatsApp | ||||||
|  | // Handle, for sure image/jpeg, image/png and image/gif MIME types | ||||||
|  | func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) { | ||||||
|  | 	fi := msg.Extra["file"][0].(config.FileInfo) | ||||||
|  |  | ||||||
|  | 	// Post image message | ||||||
|  | 	message := whatsapp.ImageMessage{ | ||||||
|  | 		Info: whatsapp.MessageInfo{ | ||||||
|  | 			RemoteJid: msg.Channel, | ||||||
|  | 		}, | ||||||
|  | 		Type:    filetype, | ||||||
|  | 		Caption: msg.Username + fi.Comment, | ||||||
|  | 		Content: bytes.NewReader(*fi.Data), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b.Log.Debugf("=> Sending %#v", msg) | ||||||
|  |  | ||||||
|  | 	// create message ID | ||||||
|  | 	// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented | ||||||
|  | 	idBytes := make([]byte, 10) | ||||||
|  | 	if _, err := rand.Read(idBytes); err != nil { | ||||||
|  | 		b.Log.Warn(err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes)) | ||||||
|  | 	_, err := b.conn.Send(message) | ||||||
|  |  | ||||||
|  | 	return message.Info.Id, err | ||||||
| } | } | ||||||
|  |  | ||||||
| // Send a message from the bridge to WhatsApp | // Send a message from the bridge to WhatsApp | ||||||
| @@ -241,14 +281,12 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) { | |||||||
| 		if msg.ID == "" { | 		if msg.ID == "" { | ||||||
| 			// No message ID in case action is executed on a message sent before the bridge was started | 			// No message ID in case action is executed on a message sent before the bridge was started | ||||||
| 			// and then the bridge cache doesn't have this message ID mapped | 			// and then the bridge cache doesn't have this message ID mapped | ||||||
|  |  | ||||||
| 			// TODO 42wim Doesn't the app get clogged with a ton of IDs after some time of running? |  | ||||||
| 			// WhatsApp allows to set any ID so in that case we could use external IDs and don't do mapping |  | ||||||
| 			// but external IDs are not set |  | ||||||
| 			return "", nil | 			return "", nil | ||||||
| 		} | 		} | ||||||
| 		// TODO delete message on WhatsApp https://github.com/Rhymen/go-whatsapp/issues/100 |  | ||||||
| 		return "", nil | 		_, err := b.conn.RevokeMessage(msg.Channel, msg.ID, true) | ||||||
|  |  | ||||||
|  | 		return "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Edit message | 	// Edit message | ||||||
| @@ -256,21 +294,27 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) { | |||||||
| 		b.Log.Debugf("updating message with id %s", msg.ID) | 		b.Log.Debugf("updating message with id %s", msg.ID) | ||||||
|  |  | ||||||
| 		msg.Text += " (edited)" | 		msg.Text += " (edited)" | ||||||
| 		// TODO handle edit as a message reply with updated text |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	//// TODO Handle Upload a file | 	// Handle Upload a file | ||||||
| 	//if msg.Extra != nil { | 	if msg.Extra["file"] != nil { | ||||||
| 	//	for _, rmsg := range helper.HandleExtra(&msg, b.General) { | 		fi := msg.Extra["file"][0].(config.FileInfo) | ||||||
| 	//		b.c.SendMessage(roomID, rmsg.Username+rmsg.Text) | 		filetype := mime.TypeByExtension(filepath.Ext(fi.Name)) | ||||||
| 	//	} |  | ||||||
| 	//	if len(msg.Extra["file"]) > 0 { | 		b.Log.Debugf("Extra file is %#v", filetype) | ||||||
| 	//		return b.handleUploadFile(&msg, roomID) |  | ||||||
| 	//	} | 		// TODO: add different types | ||||||
| 	//} | 		// TODO: add webp conversion | ||||||
|  | 		switch filetype { | ||||||
|  | 		case "image/jpeg", "image/png", "image/gif": | ||||||
|  | 			return b.PostImageMessage(msg, filetype) | ||||||
|  | 		default: | ||||||
|  | 			return b.PostDocumentMessage(msg, filetype) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Post text message | 	// Post text message | ||||||
| 	text := whatsapp.TextMessage{ | 	message := whatsapp.TextMessage{ | ||||||
| 		Info: whatsapp.MessageInfo{ | 		Info: whatsapp.MessageInfo{ | ||||||
| 			RemoteJid: msg.Channel, // which equals to group id | 			RemoteJid: msg.Channel, // which equals to group id | ||||||
| 		}, | 		}, | ||||||
| @@ -279,17 +323,7 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) { | |||||||
|  |  | ||||||
| 	b.Log.Debugf("=> Sending %#v", msg) | 	b.Log.Debugf("=> Sending %#v", msg) | ||||||
|  |  | ||||||
| 	// create message ID | 	return b.conn.Send(message) | ||||||
| 	// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented |  | ||||||
| 	bytes := make([]byte, 10) |  | ||||||
| 	if _, err := rand.Read(bytes); err != nil { |  | ||||||
| 		b.Log.Warn(err.Error()) |  | ||||||
| 	} |  | ||||||
| 	text.Info.Id = strings.ToUpper(hex.EncodeToString(bytes)) |  | ||||||
|  |  | ||||||
| 	_, err := b.conn.Send(text) |  | ||||||
|  |  | ||||||
| 	return text.Info.Id, err |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76 | // TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76 | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								bridge/xmpp/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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 | package bxmpp | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"crypto/tls" | 	"crypto/tls" | ||||||
|  | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -23,12 +27,17 @@ type Bxmpp struct { | |||||||
| 	xmppMap   map[string]string | 	xmppMap   map[string]string | ||||||
| 	connected bool | 	connected bool | ||||||
| 	sync.RWMutex | 	sync.RWMutex | ||||||
|  |  | ||||||
|  | 	avatarAvailability map[string]bool | ||||||
|  | 	avatarMap          map[string]string | ||||||
| } | } | ||||||
|  |  | ||||||
| func New(cfg *bridge.Config) bridge.Bridger { | func New(cfg *bridge.Config) bridge.Bridger { | ||||||
| 	return &Bxmpp{ | 	return &Bxmpp{ | ||||||
| 		Config:  cfg, | 		Config:             cfg, | ||||||
| 		xmppMap: make(map[string]string), | 		xmppMap:            make(map[string]string), | ||||||
|  | 		avatarAvailability: make(map[string]bool), | ||||||
|  | 		avatarMap:          make(map[string]string), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -67,17 +76,35 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { | |||||||
| 	if msg.Event == config.EventMsgDelete { | 	if msg.Event == config.EventMsgDelete { | ||||||
| 		return "", nil | 		return "", nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.Log.Debugf("=> Receiving %#v", msg) | 	b.Log.Debugf("=> Receiving %#v", msg) | ||||||
|  |  | ||||||
|  | 	if msg.Event == config.EventAvatarDownload { | ||||||
|  | 		return b.cacheAvatar(&msg), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Make a action /me of the message, prepend the username with it. | ||||||
|  | 	// https://xmpp.org/extensions/xep-0245.html | ||||||
|  | 	if msg.Event == config.EventUserAction { | ||||||
|  | 		msg.Username = "/me " + msg.Username | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support). | 	// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support). | ||||||
|  | 	var err error | ||||||
| 	if msg.Extra != nil { | 	if msg.Extra != nil { | ||||||
| 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | 		for _, rmsg := range helper.HandleExtra(&msg, b.General) { | ||||||
| 			b.Log.Debugf("=> Sending attachement message %#v", rmsg) | 			b.Log.Debugf("=> Sending attachement message %#v", rmsg) | ||||||
| 			if _, err := b.xc.Send(xmpp.Chat{ | 			if b.GetString("WebhookURL") != "" { | ||||||
| 				Type:   "groupchat", | 				err = b.postSlackCompatibleWebhook(msg) | ||||||
| 				Remote: rmsg.Channel + "@" + b.GetString("Muc"), | 			} else { | ||||||
| 				Text:   rmsg.Username + rmsg.Text, | 				_, err = b.xc.Send(xmpp.Chat{ | ||||||
| 			}); err != nil { | 					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.") | 				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 | 	var msgReplaceID string | ||||||
| 	msgID := xid.New().String() | 	msgID := xid.New().String() | ||||||
| 	if msg.ID != "" { | 	if msg.ID != "" { | ||||||
| 		msgID = msg.ID |  | ||||||
| 		msgReplaceID = msg.ID | 		msgReplaceID = msg.ID | ||||||
| 	} | 	} | ||||||
| 	// Post normal message. |  | ||||||
| 	b.Log.Debugf("=> Sending message %#v", msg) | 	b.Log.Debugf("=> Sending message %#v", msg) | ||||||
| 	if _, err := b.xc.Send(xmpp.Chat{ | 	if _, err := b.xc.Send(xmpp.Chat{ | ||||||
| 		Type:      "groupchat", | 		Type:      "groupchat", | ||||||
| @@ -106,28 +143,64 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { | |||||||
| 	return msgID, nil | 	return msgID, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bxmpp) createXMPP() error { | func (b *Bxmpp) postSlackCompatibleWebhook(msg config.Message) error { | ||||||
| 	if !strings.Contains(b.GetString("Jid"), "@") { | 	type XMPPWebhook struct { | ||||||
| 		return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid")) | 		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{ | 	tc := &tls.Config{ | ||||||
| 		ServerName:         strings.Split(b.GetString("Jid"), "@")[1], | 		ServerName:         serverName, | ||||||
| 		InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec | 		InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	xmpp.DebugWriter = b.Log.Writer() | ||||||
|  |  | ||||||
| 	options := xmpp.Options{ | 	options := xmpp.Options{ | ||||||
| 		Host:                         b.GetString("Server"), | 		Host:                         b.GetString("Server"), | ||||||
| 		User:                         b.GetString("Jid"), | 		User:                         b.GetString("Jid"), | ||||||
| 		Password:                     b.GetString("Password"), | 		Password:                     b.GetString("Password"), | ||||||
| 		NoTLS:                        true, | 		NoTLS:                        true, | ||||||
| 		StartTLS:                     true, | 		StartTLS:                     !b.GetBool("NoTLS"), | ||||||
| 		TLSConfig:                    tc, | 		TLSConfig:                    tc, | ||||||
| 		Debug:                        b.GetBool("debug"), | 		Debug:                        b.GetBool("debug"), | ||||||
| 		Logger:                       b.Log.Writer(), |  | ||||||
| 		Session:                      true, | 		Session:                      true, | ||||||
| 		Status:                       "", | 		Status:                       "", | ||||||
| 		StatusMessage:                "", | 		StatusMessage:                "", | ||||||
| 		Resource:                     "", | 		Resource:                     "", | ||||||
| 		InsecureAllowUnencryptedAuth: false, | 		InsecureAllowUnencryptedAuth: b.GetBool("NoTLS"), | ||||||
| 	} | 	} | ||||||
| 	var err error | 	var err error | ||||||
| 	b.xc, err = options.NewClient() | 	b.xc, err = options.NewClient() | ||||||
| @@ -210,7 +283,13 @@ func (b *Bxmpp) handleXMPP() error { | |||||||
| 	for { | 	for { | ||||||
| 		m, err := b.xc.Recv() | 		m, err := b.xc.Recv() | ||||||
| 		if err != nil { | 		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) { | 		switch v := m.(type) { | ||||||
| @@ -228,6 +307,16 @@ func (b *Bxmpp) handleXMPP() error { | |||||||
| 					event = config.EventTopicChange | 					event = config.EventTopicChange | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | 				available, sok := b.avatarAvailability[v.Remote] | ||||||
|  | 				avatar := "" | ||||||
|  | 				if !sok { | ||||||
|  | 					b.Log.Debugf("Requesting avatar data") | ||||||
|  | 					b.avatarAvailability[v.Remote] = false | ||||||
|  | 					b.xc.AvatarRequestData(v.Remote) | ||||||
|  | 				} else if available { | ||||||
|  | 					avatar = getAvatar(b.avatarMap, v.Remote, b.General) | ||||||
|  | 				} | ||||||
|  |  | ||||||
| 				msgID := v.ID | 				msgID := v.ID | ||||||
| 				if v.ReplaceID != "" { | 				if v.ReplaceID != "" { | ||||||
| 					msgID = v.ReplaceID | 					msgID = v.ReplaceID | ||||||
| @@ -237,6 +326,7 @@ func (b *Bxmpp) handleXMPP() error { | |||||||
| 					Text:     v.Text, | 					Text:     v.Text, | ||||||
| 					Channel:  b.parseChannel(v.Remote), | 					Channel:  b.parseChannel(v.Remote), | ||||||
| 					Account:  b.Account, | 					Account:  b.Account, | ||||||
|  | 					Avatar:   avatar, | ||||||
| 					UserID:   v.Remote, | 					UserID:   v.Remote, | ||||||
| 					ID:       msgID, | 					ID:       msgID, | ||||||
| 					Event:    event, | 					Event:    event, | ||||||
| @@ -253,6 +343,10 @@ func (b *Bxmpp) handleXMPP() error { | |||||||
| 				b.Log.Debugf("<= Message is %#v", rmsg) | 				b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
| 				b.Remote <- rmsg | 				b.Remote <- rmsg | ||||||
| 			} | 			} | ||||||
|  | 		case xmpp.AvatarData: | ||||||
|  | 			b.handleDownloadAvatar(v) | ||||||
|  | 			b.avatarAvailability[v.From] = true | ||||||
|  | 			b.Log.Debugf("Avatar for %s is now available", v.From) | ||||||
| 		case xmpp.Presence: | 		case xmpp.Presence: | ||||||
| 			// Do nothing. | 			// Do nothing. | ||||||
| 		} | 		} | ||||||
| @@ -306,7 +400,7 @@ func (b *Bxmpp) handleUploadFile(msg *config.Message) error { | |||||||
|  |  | ||||||
| func (b *Bxmpp) parseNick(remote string) string { | func (b *Bxmpp) parseNick(remote string) string { | ||||||
| 	s := strings.Split(remote, "@") | 	s := strings.Split(remote, "@") | ||||||
| 	if len(s) > 0 { | 	if len(s) > 1 { | ||||||
| 		s = strings.Split(s[1], "/") | 		s = strings.Split(s[1], "/") | ||||||
| 		if len(s) == 2 { | 		if len(s) == 2 { | ||||||
| 			return s[1] // nick | 			return s[1] // nick | ||||||
| @@ -345,6 +439,11 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool { | |||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Ignore messages posted by our webhook | ||||||
|  | 	if b.GetString("WebhookURL") != "" && strings.Contains(message.ID, "webhookbot") { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// skip delayed messages | 	// skip delayed messages | ||||||
| 	return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5 | 	return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package bzulip | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| @@ -11,6 +12,7 @@ import ( | |||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/bridge/helper" | 	"github.com/42wim/matterbridge/bridge/helper" | ||||||
|  | 	"github.com/42wim/matterbridge/version" | ||||||
| 	gzb "github.com/matterbridge/gozulipbot" | 	gzb "github.com/matterbridge/gozulipbot" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -27,7 +29,7 @@ func New(cfg *bridge.Config) bridge.Bridger { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (b *Bzulip) Connect() error { | func (b *Bzulip) Connect() error { | ||||||
| 	bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login")} | 	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() | 	bot.Init() | ||||||
| 	q, err := bot.RegisterAll() | 	q, err := bot.RegisterAll() | ||||||
| 	b.q = q | 	b.q = q | ||||||
| @@ -125,6 +127,7 @@ func (b *Bzulip) handleQueue() error { | |||||||
| 			b.Log.Debug("heartbeat received.") | 			b.Log.Debug("heartbeat received.") | ||||||
| 		default: | 		default: | ||||||
| 			b.Log.Debugf("receiving error: %#v", err) | 			b.Log.Debugf("receiving error: %#v", err) | ||||||
|  | 			time.Sleep(time.Second * 10) | ||||||
| 		} | 		} | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			continue | 			continue | ||||||
| @@ -135,19 +138,25 @@ func (b *Bzulip) handleQueue() error { | |||||||
| 			if m.SenderEmail == b.GetString("login") { | 			if m.SenderEmail == b.GetString("login") { | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			avatarURL := m.AvatarURL | ||||||
|  | 			if !strings.HasPrefix(avatarURL, "http") { | ||||||
|  | 				avatarURL = b.GetString("server") + avatarURL | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			rmsg := config.Message{ | 			rmsg := config.Message{ | ||||||
| 				Username: m.SenderFullName, | 				Username: m.SenderFullName, | ||||||
| 				Text:     m.Content, | 				Text:     m.Content, | ||||||
| 				Channel:  b.getChannel(m.StreamID) + "/topic:" + m.Subject, | 				Channel:  b.getChannel(m.StreamID) + "/topic:" + m.Subject, | ||||||
| 				Account:  b.Account, | 				Account:  b.Account, | ||||||
| 				UserID:   strconv.Itoa(m.SenderID), | 				UserID:   strconv.Itoa(m.SenderID), | ||||||
| 				Avatar:   m.AvatarURL, | 				Avatar:   avatarURL, | ||||||
| 			} | 			} | ||||||
| 			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) | 			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) | ||||||
| 			b.Log.Debugf("<= Message is %#v", rmsg) | 			b.Log.Debugf("<= Message is %#v", rmsg) | ||||||
| 			b.Remote <- rmsg | 			b.Remote <- rmsg | ||||||
| 			b.q.LastEventID = m.ID |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		time.Sleep(time.Second * 3) | 		time.Sleep(time.Second * 3) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										1652
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										1652
									
								
								changelog.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,30 +0,0 @@ | |||||||
| #!/usr/bin/env bash |  | ||||||
| set -u -e -x -o pipefail |  | ||||||
|  |  | ||||||
| go version | grep go1.13 || 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
									
									
									
									
									
								
							
							
						
						
									
										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
									
									
									
									
									
								
							
							
						
						
									
										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 |  | ||||||
							
								
								
									
										10
									
								
								contrib/outmessage-discordemoji.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								contrib/outmessage-discordemoji.tengo
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | text := import("text") | ||||||
|  |  | ||||||
|  | // if we're not sending to a discord bridge, | ||||||
|  | // then convert custom emoji tags into url's | ||||||
|  | if (inProtocol == "discord" && outProtocol != "discord") { | ||||||
|  |     rePNG := text.re_compile(`<:.*?:([0-9]+)>`) | ||||||
|  |     msgText=rePNG.replace(msgText,"https://cdn.discordapp.com/emojis/$1.png") | ||||||
|  |     reGIF := text.re_compile(`<a:.*?:([0-9]+)>`) | ||||||
|  |     msgText=reGIF.replace(msgText,"https://cdn.discordapp.com/emojis/$1.gif") | ||||||
|  | } | ||||||
| @@ -1,9 +1,10 @@ | |||||||
| FROM alpine:edge as certs | FROM alpine:edge as certs | ||||||
| RUN apk --update add ca-certificates | RUN apk --update add ca-certificates | ||||||
|  | ARG VERSION=1.22.3 | ||||||
|  | ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-${VERSION}-linux-arm64 /bin/matterbridge | ||||||
|  | RUN chmod +x /bin/matterbridge | ||||||
|  |  | ||||||
| FROM scratch | FROM scratch | ||||||
| ARG VERSION=1.12.3 |  | ||||||
| COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt | COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt | ||||||
| ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-linux-arm /bin/matterbridge | COPY --from=certs /bin/matterbridge /bin/matterbridge | ||||||
| RUN chmod +x /bin/matterbridge |  | ||||||
| ENTRYPOINT ["/bin/matterbridge"] | ENTRYPOINT ["/bin/matterbridge"] | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !noapi | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/42wim/matterbridge/bridge/api" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["api"] = api.New | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								gateway/bridgemap/bdiscord.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								gateway/bridgemap/bdiscord.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | // +build !nodiscord | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bdiscord "github.com/42wim/matterbridge/bridge/discord" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["discord"] = bdiscord.New | ||||||
|  | 	UserTypingSupport["discord"] = struct{}{} | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bgitter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bgitter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +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
									
								
							
							
						
						
									
										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/birc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/birc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !noirc | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	birc "github.com/42wim/matterbridge/bridge/irc" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["irc"] = birc.New | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bkeybase.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bkeybase.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !nokeybase | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bkeybase "github.com/42wim/matterbridge/bridge/keybase" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["keybase"] = bkeybase.New | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bmatrix.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bmatrix.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !nomatrix | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bmatrix "github.com/42wim/matterbridge/bridge/matrix" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["matrix"] = bmatrix.New | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bmattermost.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bmattermost.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !nomattermost | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bmattermost "github.com/42wim/matterbridge/bridge/mattermost" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["mattermost"] = bmattermost.New | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bmsteams.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bmsteams.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !nomsteams | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bmsteams "github.com/42wim/matterbridge/bridge/msteams" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["msteams"] = bmsteams.New | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bmumble.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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 | ||||||
|  | } | ||||||
| @@ -2,45 +2,9 @@ package bridgemap | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/bridge/api" |  | ||||||
| 	bdiscord "github.com/42wim/matterbridge/bridge/discord" |  | ||||||
| 	bgitter "github.com/42wim/matterbridge/bridge/gitter" |  | ||||||
| 	birc "github.com/42wim/matterbridge/bridge/irc" |  | ||||||
| 	bkeybase "github.com/42wim/matterbridge/bridge/keybase" |  | ||||||
| 	bmatrix "github.com/42wim/matterbridge/bridge/matrix" |  | ||||||
| 	bmattermost "github.com/42wim/matterbridge/bridge/mattermost" |  | ||||||
| 	brocketchat "github.com/42wim/matterbridge/bridge/rocketchat" |  | ||||||
| 	bslack "github.com/42wim/matterbridge/bridge/slack" |  | ||||||
| 	bsshchat "github.com/42wim/matterbridge/bridge/sshchat" |  | ||||||
| 	bsteam "github.com/42wim/matterbridge/bridge/steam" |  | ||||||
| 	btelegram "github.com/42wim/matterbridge/bridge/telegram" |  | ||||||
| 	bwhatsapp "github.com/42wim/matterbridge/bridge/whatsapp" |  | ||||||
| 	bxmpp "github.com/42wim/matterbridge/bridge/xmpp" |  | ||||||
| 	bzulip "github.com/42wim/matterbridge/bridge/zulip" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	FullMap = map[string]bridge.Factory{ | 	FullMap           = map[string]bridge.Factory{} | ||||||
| 		"api":          api.New, | 	UserTypingSupport = map[string]struct{}{} | ||||||
| 		"discord":      bdiscord.New, |  | ||||||
| 		"gitter":       bgitter.New, |  | ||||||
| 		"irc":          birc.New, |  | ||||||
| 		"mattermost":   bmattermost.New, |  | ||||||
| 		"matrix":       bmatrix.New, |  | ||||||
| 		"rocketchat":   brocketchat.New, |  | ||||||
| 		"slack-legacy": bslack.NewLegacy, |  | ||||||
| 		"slack":        bslack.New, |  | ||||||
| 		"sshchat":      bsshchat.New, |  | ||||||
| 		"steam":        bsteam.New, |  | ||||||
| 		"telegram":     btelegram.New, |  | ||||||
| 		"whatsapp":     bwhatsapp.New, |  | ||||||
| 		"xmpp":         bxmpp.New, |  | ||||||
| 		"zulip":        bzulip.New, |  | ||||||
| 		"keybase":      bkeybase.New, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	UserTypingSupport = map[string]struct{}{ |  | ||||||
| 		"slack":   {}, |  | ||||||
| 		"discord": {}, |  | ||||||
| 	} |  | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/brocketchat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/brocketchat.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !norocketchat | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	brocketchat "github.com/42wim/matterbridge/bridge/rocketchat" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["rocketchat"] = brocketchat.New | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								gateway/bridgemap/bslack.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								gateway/bridgemap/bslack.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | // +build !noslack | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bslack "github.com/42wim/matterbridge/bridge/slack" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["slack-legacy"] = bslack.NewLegacy | ||||||
|  | 	FullMap["slack"] = bslack.New | ||||||
|  | 	UserTypingSupport["slack"] = struct{}{} | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bsshchat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bsshchat.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !nosshchat | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bsshchat "github.com/42wim/matterbridge/bridge/sshchat" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["sshchat"] = bsshchat.New | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bsteam.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bsteam.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !nosteam | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bsteam "github.com/42wim/matterbridge/bridge/steam" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["steam"] = bsteam.New | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/btelegram.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/btelegram.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !notelegram | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	btelegram "github.com/42wim/matterbridge/bridge/telegram" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["telegram"] = btelegram.New | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bvk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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 | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bwhatsapp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bwhatsapp.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !nowhatsapp | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bwhatsapp "github.com/42wim/matterbridge/bridge/whatsapp" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["whatsapp"] = bwhatsapp.New | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bxmpp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bxmpp.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !noxmpp | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bxmpp "github.com/42wim/matterbridge/bridge/xmpp" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["xmpp"] = bxmpp.New | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bzulip.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bzulip.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // +build !nozulip | ||||||
|  |  | ||||||
|  | package bridgemap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	bzulip "github.com/42wim/matterbridge/bridge/zulip" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	FullMap["zulip"] = bzulip.New | ||||||
|  | } | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| package gateway | package gateway | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"os" | 	"os" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| @@ -10,10 +11,10 @@ import ( | |||||||
| 	"github.com/42wim/matterbridge/bridge" | 	"github.com/42wim/matterbridge/bridge" | ||||||
| 	"github.com/42wim/matterbridge/bridge/config" | 	"github.com/42wim/matterbridge/bridge/config" | ||||||
| 	"github.com/42wim/matterbridge/internal" | 	"github.com/42wim/matterbridge/internal" | ||||||
| 	"github.com/d5/tengo/script" | 	"github.com/d5/tengo/v2" | ||||||
| 	"github.com/d5/tengo/stdlib" | 	"github.com/d5/tengo/v2/stdlib" | ||||||
| 	lru "github.com/hashicorp/golang-lru" | 	lru "github.com/hashicorp/golang-lru" | ||||||
| 	"github.com/matterbridge/emoji" | 	"github.com/kyokomi/emoji/v2" | ||||||
| 	"github.com/sirupsen/logrus" | 	"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 { | func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string { | ||||||
| 	ID := protocol + " " + mID | 	ID := protocol + " " + mID | ||||||
| 	if gw.Messages.Contains(ID) { | 	if gw.Messages.Contains(ID) { | ||||||
| 		return mID | 		return ID | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If not keyed, iterate through cache for downstream, and infer upstream. | 	// 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) | 		ids := v.([]*BrMsgID) | ||||||
| 		for _, downstreamMsgObj := range ids { | 		for _, downstreamMsgObj := range ids { | ||||||
| 			if ID == downstreamMsgObj.ID { | 			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) { | func (gw *Gateway) checkConfig(cfg *config.Bridge) { | ||||||
| 	match := false | 	match := false | ||||||
| 	for _, key := range gw.Router.Config.Viper().AllKeys() { | 	for _, key := range gw.Router.Config.Viper().AllKeys() { | ||||||
| 		if strings.HasPrefix(key, cfg.Account) { | 		if strings.HasPrefix(key, strings.ToLower(cfg.Account)) { | ||||||
| 			match = true | 			match = true | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| @@ -126,7 +127,7 @@ func (gw *Gateway) AddConfig(cfg *config.Gateway) error { | |||||||
| 		gw.logger.Errorf("mapChannels() failed: %s", err) | 		gw.logger.Errorf("mapChannels() failed: %s", err) | ||||||
| 	} | 	} | ||||||
| 	for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) { | 	for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) { | ||||||
| 		br := br //scopelint | 		br := br // scopelint | ||||||
| 		err := gw.AddBridge(&br) | 		err := gw.AddBridge(&br) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| @@ -306,8 +307,6 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string { | func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string { | ||||||
| 	br := gw.Bridges[msg.Account] |  | ||||||
| 	msg.Protocol = br.Protocol |  | ||||||
| 	if dest.GetBool("StripNick") { | 	if dest.GetBool("StripNick") { | ||||||
| 		re := regexp.MustCompile("[^a-zA-Z0-9]+") | 		re := regexp.MustCompile("[^a-zA-Z0-9]+") | ||||||
| 		msg.Username = re.ReplaceAllString(msg.Username, "") | 		msg.Username = re.ReplaceAllString(msg.Username, "") | ||||||
| @@ -315,6 +314,7 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri | |||||||
| 	nick := dest.GetString("RemoteNickFormat") | 	nick := dest.GetString("RemoteNickFormat") | ||||||
|  |  | ||||||
| 	// loop to replace nicks | 	// loop to replace nicks | ||||||
|  | 	br := gw.Bridges[msg.Account] | ||||||
| 	for _, outer := range br.GetStringSlice2D("ReplaceNicks") { | 	for _, outer := range br.GetStringSlice2D("ReplaceNicks") { | ||||||
| 		search := outer[0] | 		search := outer[0] | ||||||
| 		replace := outer[1] | 		replace := outer[1] | ||||||
| @@ -337,20 +337,21 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri | |||||||
| 			} | 			} | ||||||
| 			i++ | 			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.ReplaceAll(nick, "{BRIDGE}", br.Name) | ||||||
| 	nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1) | 	nick = strings.ReplaceAll(nick, "{PROTOCOL}", br.Protocol) | ||||||
| 	nick = strings.Replace(nick, "{GATEWAY}", gw.Name, -1) | 	nick = strings.ReplaceAll(nick, "{GATEWAY}", gw.Name) | ||||||
| 	nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1) | 	nick = strings.ReplaceAll(nick, "{LABEL}", br.GetString("Label")) | ||||||
| 	nick = strings.Replace(nick, "{NICK}", msg.Username, -1) | 	nick = strings.ReplaceAll(nick, "{NICK}", msg.Username) | ||||||
| 	nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1) | 	nick = strings.ReplaceAll(nick, "{USERID}", msg.UserID) | ||||||
|  | 	nick = strings.ReplaceAll(nick, "{CHANNEL}", msg.Channel) | ||||||
| 	tengoNick, err := gw.modifyUsernameTengo(msg, br) | 	tengoNick, err := gw.modifyUsernameTengo(msg, br) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		gw.logger.Errorf("modifyUsernameTengo error: %s", err) | 		gw.logger.Errorf("modifyUsernameTengo error: %s", err) | ||||||
| 	} | 	} | ||||||
| 	nick = strings.Replace(nick, "{TENGO}", tengoNick, -1) //nolint:gocritic | 	nick = strings.ReplaceAll(nick, "{TENGO}", tengoNick) | ||||||
| 	return nick | 	return nick | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -364,14 +365,28 @@ func (gw *Gateway) modifyAvatar(msg *config.Message, dest *bridge.Bridge) string | |||||||
| } | } | ||||||
|  |  | ||||||
| func (gw *Gateway) modifyMessage(msg *config.Message) { | func (gw *Gateway) modifyMessage(msg *config.Message) { | ||||||
| 	if 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) | 		gw.logger.Errorf("TengoModifyMessage failed: %s", err) | ||||||
| 	} | 	} | ||||||
| 	if err := modifyMessageTengo(gw.BridgeValues().Tengo.Message, msg); err != nil { |  | ||||||
|  | 	inMessage := gw.BridgeValues().Tengo.InMessage | ||||||
|  | 	if inMessage == "" { | ||||||
|  | 		inMessage = gw.BridgeValues().Tengo.Message | ||||||
|  | 		if inMessage != "" { | ||||||
|  | 			gw.logger.Warnf("Tengo Message=%s is deprecated and will be removed in v1.20.0, please move to Tengo InMessage=%s", inMessage, inMessage) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := modifyInMessageTengo(inMessage, msg); err != nil { | ||||||
| 		gw.logger.Errorf("Tengo.Message failed: %s", err) | 		gw.logger.Errorf("Tengo.Message failed: %s", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// replace :emoji: to unicode | 	// replace :emoji: to unicode | ||||||
|  | 	emoji.ReplacePadding = "" | ||||||
| 	msg.Text = emoji.Sprint(msg.Text) | 	msg.Text = emoji.Sprint(msg.Text) | ||||||
|  |  | ||||||
| 	br := gw.Bridges[msg.Account] | 	br := gw.Bridges[msg.Account] | ||||||
| @@ -417,44 +432,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 | 	// Too noisy to log like other events | ||||||
|  | 	debugSendMessage := "" | ||||||
| 	if msg.Event != config.EventUserTyping { | 	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.Channel = channel.Name | ||||||
| 	msg.Avatar = gw.modifyAvatar(rmsg, dest) | 	msg.Avatar = gw.modifyAvatar(rmsg, dest) | ||||||
| 	msg.Username = gw.modifyUsername(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 | 	// for api we need originchannel as channel | ||||||
| 	if dest.Protocol == apiProtocol { | 	if dest.Protocol == apiProtocol { | ||||||
| 		msg.Channel = rmsg.Channel | 		msg.Channel = rmsg.Channel | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel) | 	msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel) | ||||||
| 	if msg.ParentID == "" { | 	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 | 	// 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 != "" { | 	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 { | 	if err != nil { | ||||||
| 		gw.logger.Errorf("modifySendMessageTengo: %s", err) | 		gw.logger.Errorf("modifySendMessageTengo: %s", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if drop { | ||||||
|  | 		gw.logger.Debugf("=> Tengo dropping %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name) | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if debugSendMessage != "" { | ||||||
|  | 		gw.logger.Debug(debugSendMessage) | ||||||
|  | 	} | ||||||
| 	// if we are using mattermost plugin account, send messages to MattermostPlugin channel | 	// if we are using mattermost plugin account, send messages to MattermostPlugin channel | ||||||
| 	// that can be picked up by the mattermost matterbridge plugin | 	// that can be picked up by the mattermost matterbridge plugin | ||||||
| 	if dest.Account == "mattermost.plugin" { | 	if dest.Account == "mattermost.plugin" { | ||||||
| 		gw.Router.MattermostPlugin <- msg | 		gw.Router.MattermostPlugin <- msg | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	defer func(t time.Time) { | ||||||
|  | 		gw.logger.Debugf("=> Send from %s (%s) to %s (%s) took %s", msg.Account, rmsg.Channel, dest.Account, channel.Name, time.Since(t)) | ||||||
|  | 	}(time.Now()) | ||||||
|  |  | ||||||
| 	mID, err := dest.Send(msg) | 	mID, err := dest.Send(msg) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return mID, err | 		return mID, err | ||||||
| @@ -464,7 +500,7 @@ func (gw *Gateway) SendMessage( | |||||||
| 	if mID != "" { | 	if mID != "" { | ||||||
| 		gw.logger.Debugf("mID %s: %s", dest.Account, mID) | 		gw.logger.Debugf("mID %s: %s", dest.Account, mID) | ||||||
| 		return mID, nil | 		return mID, nil | ||||||
| 		//brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID}) | 		// brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID}) | ||||||
| 	} | 	} | ||||||
| 	return "", nil | 	return "", nil | ||||||
| } | } | ||||||
| @@ -506,7 +542,7 @@ func getProtocol(msg *config.Message) string { | |||||||
| 	return p[0] | 	return p[0] | ||||||
| } | } | ||||||
|  |  | ||||||
| func modifyMessageTengo(filename string, msg *config.Message) error { | func modifyInMessageTengo(filename string, msg *config.Message) error { | ||||||
| 	if filename == "" { | 	if filename == "" { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| @@ -514,10 +550,11 @@ func modifyMessageTengo(filename string, msg *config.Message) error { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	s := script.New(res) | 	s := tengo.NewScript(res) | ||||||
| 	s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) | 	s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) | ||||||
| 	_ = s.Add("msgText", msg.Text) | 	_ = s.Add("msgText", msg.Text) | ||||||
| 	_ = s.Add("msgUsername", msg.Username) | 	_ = s.Add("msgUsername", msg.Username) | ||||||
|  | 	_ = s.Add("msgUserID", msg.UserID) | ||||||
| 	_ = s.Add("msgAccount", msg.Account) | 	_ = s.Add("msgAccount", msg.Account) | ||||||
| 	_ = s.Add("msgChannel", msg.Channel) | 	_ = s.Add("msgChannel", msg.Channel) | ||||||
| 	c, err := s.Compile() | 	c, err := s.Compile() | ||||||
| @@ -541,11 +578,12 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) ( | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 	s := script.New(res) | 	s := tengo.NewScript(res) | ||||||
| 	s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) | 	s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) | ||||||
| 	_ = s.Add("result", "") | 	_ = s.Add("result", "") | ||||||
| 	_ = s.Add("msgText", msg.Text) | 	_ = s.Add("msgText", msg.Text) | ||||||
| 	_ = s.Add("msgUsername", msg.Username) | 	_ = s.Add("msgUsername", msg.Username) | ||||||
|  | 	_ = s.Add("msgUserID", msg.UserID) | ||||||
| 	_ = s.Add("nick", msg.Username) | 	_ = s.Add("nick", msg.Username) | ||||||
| 	_ = s.Add("msgAccount", msg.Account) | 	_ = s.Add("msgAccount", msg.Account) | ||||||
| 	_ = s.Add("msgChannel", msg.Channel) | 	_ = s.Add("msgChannel", msg.Channel) | ||||||
| @@ -565,22 +603,28 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) ( | |||||||
| 	return c.Get("result").String(), nil | 	return c.Get("result").String(), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (gw *Gateway) 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 | 	filename := gw.BridgeValues().Tengo.OutMessage | ||||||
| 	var res []byte | 	var ( | ||||||
| 	var err error | 		res  []byte | ||||||
|  | 		err  error | ||||||
|  | 		drop bool | ||||||
|  | 	) | ||||||
|  |  | ||||||
| 	if filename == "" { | 	if filename == "" { | ||||||
| 		res, err = internal.Asset("tengo/outmessage.tengo") | 		res, err = internal.Asset("tengo/outmessage.tengo") | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return drop, err | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		res, err = ioutil.ReadFile(filename) | 		res, err = ioutil.ReadFile(filename) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return drop, err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	s := script.New(res) |  | ||||||
|  | 	s := tengo.NewScript(res) | ||||||
|  |  | ||||||
| 	s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) | 	s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) | ||||||
| 	_ = s.Add("inAccount", origmsg.Account) | 	_ = s.Add("inAccount", origmsg.Account) | ||||||
| 	_ = s.Add("inProtocol", origmsg.Protocol) | 	_ = s.Add("inProtocol", origmsg.Protocol) | ||||||
| @@ -594,14 +638,20 @@ func (gw *Gateway) modifySendMessageTengo(origmsg *config.Message, msg *config.M | |||||||
| 	_ = s.Add("outEvent", msg.Event) | 	_ = s.Add("outEvent", msg.Event) | ||||||
| 	_ = s.Add("msgText", msg.Text) | 	_ = s.Add("msgText", msg.Text) | ||||||
| 	_ = s.Add("msgUsername", msg.Username) | 	_ = s.Add("msgUsername", msg.Username) | ||||||
|  | 	_ = s.Add("msgUserID", msg.UserID) | ||||||
|  | 	_ = s.Add("msgDrop", drop) | ||||||
| 	c, err := s.Compile() | 	c, err := s.Compile() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return drop, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := c.Run(); err != nil { | 	if err := c.Run(); err != nil { | ||||||
| 		return err | 		return drop, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	drop = c.Get("msgDrop").Bool() | ||||||
| 	msg.Text = c.Get("msgText").String() | 	msg.Text = c.Get("msgText").String() | ||||||
| 	msg.Username = c.Get("msgUsername").String() | 	msg.Username = c.Get("msgUsername").String() | ||||||
| 	return nil |  | ||||||
|  | 	return drop, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -533,7 +533,7 @@ func (s *ignoreTestSuite) TestIgnoreNicks() { | |||||||
| func BenchmarkTengo(b *testing.B) { | func BenchmarkTengo(b *testing.B) { | ||||||
| 	msg := &config.Message{Username: "user", Text: "blah testing", Account: "protocol.account", Channel: "mychannel"} | 	msg := &config.Message{Username: "user", Text: "blah testing", Account: "protocol.account", Channel: "mychannel"} | ||||||
| 	for n := 0; n < b.N; n++ { | 	for n := 0; n < b.N; n++ { | ||||||
| 		err := modifyMessageTengo("bench.tengo", msg) | 		err := modifyInMessageTengo("bench.tengo", msg) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -169,7 +169,7 @@ func (gw *Gateway) ignoreEvent(event string, dest *bridge.Bridge) bool { | |||||||
| 	switch event { | 	switch event { | ||||||
| 	case config.EventAvatarDownload: | 	case config.EventAvatarDownload: | ||||||
| 		// Avatar downloads are only relevant for telegram and mattermost for now | 		// Avatar downloads are only relevant for telegram and mattermost for now | ||||||
| 		if dest.Protocol != "mattermost" && dest.Protocol != "telegram" { | 		if dest.Protocol != "mattermost" && dest.Protocol != "telegram" && dest.Protocol != "xmpp" { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| 	case config.EventJoinLeave: | 	case config.EventJoinLeave: | ||||||
| @@ -179,7 +179,7 @@ func (gw *Gateway) ignoreEvent(event string, dest *bridge.Bridge) bool { | |||||||
| 		} | 		} | ||||||
| 	case config.EventTopicChange: | 	case config.EventTopicChange: | ||||||
| 		// only relay topic change when used in some way on other side | 		// only relay topic change when used in some way on other side | ||||||
| 		if dest.GetBool("ShowTopicChange") && dest.GetBool("SyncTopic") { | 		if !dest.GetBool("ShowTopicChange") && !dest.GetBool("SyncTopic") { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -110,7 +110,9 @@ func (r *Router) disableBridge(br *bridge.Bridge, err error) bool { | |||||||
| 	if r.BridgeValues().General.IgnoreFailureOnStart { | 	if r.BridgeValues().General.IgnoreFailureOnStart { | ||||||
| 		r.logger.Error(err) | 		r.logger.Error(err) | ||||||
| 		// setting this bridge empty | 		// setting this bridge empty | ||||||
| 		*br = bridge.Bridge{} | 		*br = bridge.Bridge{ | ||||||
|  | 			Log: br.Log, | ||||||
|  | 		} | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
| @@ -132,6 +134,9 @@ func (r *Router) handleReceive() { | |||||||
| 		r.handleEventFailure(&msg) | 		r.handleEventFailure(&msg) | ||||||
| 		r.handleEventRejoinChannels(&msg) | 		r.handleEventRejoinChannels(&msg) | ||||||
|  |  | ||||||
|  | 		// Set message protocol based on the account it came from | ||||||
|  | 		msg.Protocol = r.getBridge(msg.Account).Protocol | ||||||
|  |  | ||||||
| 		filesHandled := false | 		filesHandled := false | ||||||
| 		for _, gw := range r.Gateways { | 		for _, gw := range r.Gateways { | ||||||
| 			// record all the message ID's of the different bridges | 			// record all the message ID's of the different bridges | ||||||
| @@ -157,7 +162,7 @@ func (r *Router) handleReceive() { | |||||||
| 				// For some bridges we always add/update the message ID. | 				// For some bridges we always add/update the message ID. | ||||||
| 				// This is necessary as msgIDs will change if a bridge returns | 				// This is necessary as msgIDs will change if a bridge returns | ||||||
| 				// a different ID in response to edits. | 				// a different ID in response to edits. | ||||||
| 				if !exists || msg.Protocol == "discord" { | 				if !exists { | ||||||
| 					gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs) | 					gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|   | |||||||
							
								
								
									
										188
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										188
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,69 +3,139 @@ module github.com/42wim/matterbridge | |||||||
| require ( | require ( | ||||||
| 	github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 | 	github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 | ||||||
| 	github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f | 	github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f | ||||||
| 	github.com/Jeffail/gabs v1.1.1 // indirect | 	github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f | ||||||
| 	github.com/Philipp15b/go-steam v1.0.1-0.20190816133340-b04c5a83c1c0 | 	github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560 | ||||||
| 	github.com/Rhymen/go-whatsapp v0.0.3-0.20191003184814-fc3f792c814c | 	github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c | ||||||
| 	github.com/bwmarrin/discordgo v0.19.0 | 	github.com/SevereCloud/vksdk/v2 v2.13.0 | ||||||
| 	// github.com/bwmarrin/discordgo v0.19.0 | 	github.com/d5/tengo/v2 v2.10.0 | ||||||
| 	github.com/d5/tengo v1.24.8 | 	github.com/davecgh/go-spew v1.1.1 | ||||||
| 	github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec | 	github.com/fsnotify/fsnotify v1.5.1 | ||||||
| 	github.com/fsnotify/fsnotify v1.4.7 | 	github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 | ||||||
| 	github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible | 	github.com/gomarkdown/markdown v0.0.0-20211207152620-5d6539fd8bfc | ||||||
| 	github.com/gomarkdown/markdown v0.0.0-20190912180731-281270bc6d83 | 	github.com/google/gops v0.3.22 | ||||||
| 	github.com/google/gops v0.3.6 | 	github.com/gorilla/schema v1.2.0 | ||||||
| 	github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect | 	github.com/gorilla/websocket v1.4.2 | ||||||
| 	github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect | 	github.com/harmony-development/shibshib v0.0.0-20211127182844-512296f7c548 | ||||||
| 	github.com/gorilla/schema v1.1.0 | 	github.com/hashicorp/golang-lru v0.5.4 | ||||||
| 	github.com/gorilla/websocket v1.4.1 | 	github.com/jpillora/backoff v1.0.0 | ||||||
| 	github.com/hashicorp/golang-lru v0.5.3 | 	github.com/keybase/go-keybase-chat-bot v0.0.0-20211201215354-ee4b23828b55 | ||||||
| 	github.com/hpcloud/tail v1.0.0 // indirect | 	github.com/kyokomi/emoji/v2 v2.2.8 | ||||||
| 	github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 | 	github.com/labstack/echo/v4 v4.6.3 | ||||||
| 	github.com/jtolds/gls v4.2.1+incompatible // indirect | 	github.com/lrstanley/girc v0.0.0-20211023233735-147f0ff77566 | ||||||
| 	github.com/keybase/go-keybase-chat-bot v0.0.0-20190816161829-561f10822eb2 | 	github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 | ||||||
| 	github.com/labstack/echo/v4 v4.1.10 | 	github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696 | ||||||
| 	github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 | 	github.com/matterbridge/discordgo v0.21.2-0.20210201201054-fb39a175b4f7 | ||||||
| 	github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20190210153444-cc9d05784d5d | 	github.com/matterbridge/go-xmpp v0.0.0-20211030125215-791a06c5f1be | ||||||
| 	github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible | 	github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75 | ||||||
| 	github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91 | 	github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba | ||||||
| 	github.com/matterbridge/gomatrix v0.0.0-20191026211822-6fc7accd00ca | 	github.com/matterbridge/matterclient v0.0.0-20211107234719-faca3cd42315 | ||||||
| 	github.com/matterbridge/gozulipbot v0.0.0-20190212232658-7aa251978a18 | 	github.com/mattermost/mattermost-server/v5 v5.39.3 | ||||||
| 	github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61 | 	github.com/mattermost/mattermost-server/v6 v6.3.0 | ||||||
| 	github.com/mattermost/mattermost-server v5.5.0+incompatible | 	github.com/mattn/godown v0.0.1 | ||||||
| 	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect | 	github.com/missdeer/golib v1.0.4 | ||||||
| 	github.com/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/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 | ||||||
| 	github.com/nicksnyder/go-i18n v1.4.0 // indirect |  | ||||||
| 	github.com/nlopes/slack v0.6.0 |  | ||||||
| 	//github.com/nlopes/slack v0.6.0 |  | ||||||
| 	github.com/onsi/ginkgo v1.6.0 // indirect |  | ||||||
| 	github.com/onsi/gomega v1.4.1 // indirect |  | ||||||
| 	github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c | 	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.3.0 | ||||||
| 	github.com/rs/xid v1.2.1 | 	github.com/russross/blackfriday v1.6.0 | ||||||
| 	github.com/russross/blackfriday v1.5.2 |  | ||||||
| 	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca | 	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca | ||||||
| 	github.com/shazow/ssh-chat v1.8.2 | 	github.com/shazow/ssh-chat v1.10.1 | ||||||
| 	github.com/sirupsen/logrus v1.4.2 | 	github.com/sirupsen/logrus v1.8.1 | ||||||
| 	github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect | 	github.com/slack-go/slack v0.10.0 | ||||||
| 	github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect | 	github.com/spf13/viper v1.10.1 | ||||||
| 	github.com/spf13/viper v1.4.0 | 	github.com/stretchr/testify v1.7.0 | ||||||
| 	github.com/stretchr/testify v1.4.0 | 	github.com/vincent-petithory/dataurl v1.0.0 | ||||||
| 	github.com/technoweenie/multipartstreamer v1.0.1 // indirect | 	github.com/writeas/go-strip-markdown v2.0.1+incompatible | ||||||
| 	github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect | 	github.com/yaegashi/msgraph.go v0.1.4 | ||||||
| 	github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2 | 	github.com/zfjagann/golang-ring v0.0.0-20210116075443-7c86fdb43134 | ||||||
| 	golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 // indirect | 	golang.org/x/image v0.0.0-20211028202545-6944b10bf410 | ||||||
| 	golang.org/x/image v0.0.0-20190902063713-cb417be4ba39 | 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 | ||||||
| 	golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect | 	gomod.garykim.dev/nc-talk v0.3.0 | ||||||
| 	golang.org/x/text v0.3.2 // indirect | 	gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 | ||||||
| 	gopkg.in/fsnotify.v1 v1.4.7 // indirect | 	layeh.com/gumble v0.0.0-20200818122324-146f9205029b | ||||||
| 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect |  | ||||||
| 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| replace github.com/bwmarrin/discordgo v0.19.0 => github.com/matterbridge/discordgo v0.0.0-20191026232317-01823f4ebba4 | require ( | ||||||
|  | 	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 v3.5.1+incompatible // indirect | ||||||
|  | 	github.com/dustin/go-humanize v1.0.0 // indirect | ||||||
|  | 	github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09 // indirect | ||||||
|  | 	github.com/francoispqt/gojay v1.2.13 // indirect | ||||||
|  | 	github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect | ||||||
|  | 	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect | ||||||
|  | 	github.com/golang/protobuf v1.5.2 // indirect | ||||||
|  | 	github.com/google/uuid v1.3.0 // indirect | ||||||
|  | 	github.com/gopackage/ddp v0.0.3 // indirect | ||||||
|  | 	github.com/hashicorp/errwrap v1.1.0 // indirect | ||||||
|  | 	github.com/hashicorp/go-multierror v1.1.1 // indirect | ||||||
|  | 	github.com/hashicorp/hcl v1.0.0 // indirect | ||||||
|  | 	github.com/json-iterator/go v1.1.12 // indirect | ||||||
|  | 	github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect | ||||||
|  | 	github.com/klauspost/compress v1.14.1 // indirect | ||||||
|  | 	github.com/klauspost/cpuid/v2 v2.0.9 // indirect | ||||||
|  | 	github.com/labstack/gommon v0.3.1 // indirect | ||||||
|  | 	github.com/magiconair/properties v1.8.5 // indirect | ||||||
|  | 	github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect | ||||||
|  | 	github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect | ||||||
|  | 	github.com/mattermost/logr v1.0.13 // indirect | ||||||
|  | 	github.com/mattermost/logr/v2 v2.0.15 // indirect | ||||||
|  | 	github.com/mattn/go-colorable v0.1.12 // indirect | ||||||
|  | 	github.com/mattn/go-isatty v0.0.14 // indirect | ||||||
|  | 	github.com/mattn/go-runewidth v0.0.13 // indirect | ||||||
|  | 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect | ||||||
|  | 	github.com/minio/md5-simd v1.1.2 // indirect | ||||||
|  | 	github.com/minio/minio-go/v7 v7.0.16 // indirect | ||||||
|  | 	github.com/minio/sha256-simd v1.0.0 // indirect | ||||||
|  | 	github.com/mitchellh/go-homedir v1.1.0 // indirect | ||||||
|  | 	github.com/mitchellh/mapstructure v1.4.3 // indirect | ||||||
|  | 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||||
|  | 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||||
|  | 	github.com/monaco-io/request v1.0.5 // indirect | ||||||
|  | 	github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d // indirect | ||||||
|  | 	github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect | ||||||
|  | 	github.com/pborman/uuid v1.2.1 // indirect | ||||||
|  | 	github.com/pelletier/go-toml v1.9.4 // indirect | ||||||
|  | 	github.com/philhofer/fwd v1.1.1 // indirect | ||||||
|  | 	github.com/pkg/errors v0.9.1 // indirect | ||||||
|  | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
|  | 	github.com/rickb777/date v1.12.4 // indirect | ||||||
|  | 	github.com/rickb777/plural v1.2.0 // indirect | ||||||
|  | 	github.com/rivo/uniseg v0.2.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-20200617195104-da1b6568686e // indirect | ||||||
|  | 	github.com/spf13/afero v1.6.0 // indirect | ||||||
|  | 	github.com/spf13/cast v1.4.1 // indirect | ||||||
|  | 	github.com/spf13/jwalterweatherman v1.1.0 // indirect | ||||||
|  | 	github.com/spf13/pflag v1.0.5 // indirect | ||||||
|  | 	github.com/subosito/gotenv v1.2.0 // indirect | ||||||
|  | 	github.com/tinylib/msgp v1.1.6 // indirect | ||||||
|  | 	github.com/valyala/bytebufferpool v1.0.0 // indirect | ||||||
|  | 	github.com/valyala/fasttemplate v1.2.1 // indirect | ||||||
|  | 	github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect | ||||||
|  | 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect | ||||||
|  | 	github.com/wiggin77/cfg v1.0.2 // indirect | ||||||
|  | 	github.com/wiggin77/merror v1.0.3 // indirect | ||||||
|  | 	github.com/wiggin77/srslog v1.0.1 // indirect | ||||||
|  | 	go.uber.org/atomic v1.9.0 // indirect | ||||||
|  | 	go.uber.org/multierr v1.7.0 // indirect | ||||||
|  | 	go.uber.org/zap v1.17.0 // indirect | ||||||
|  | 	golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect | ||||||
|  | 	golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 // indirect | ||||||
|  | 	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect | ||||||
|  | 	golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect | ||||||
|  | 	golang.org/x/text v0.3.7 // indirect | ||||||
|  | 	golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect | ||||||
|  | 	google.golang.org/appengine v1.6.7 // indirect | ||||||
|  | 	google.golang.org/protobuf v1.27.1 // indirect | ||||||
|  | 	gopkg.in/ini.v1 v1.66.2 // indirect | ||||||
|  | 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect | ||||||
|  | 	gopkg.in/yaml.v2 v2.4.0 // indirect | ||||||
|  | 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect | ||||||
|  | ) | ||||||
|  |  | ||||||
| replace github.com/nlopes/slack v0.6.0 => github.com/matterbridge/slack v0.1.1-0.20191113220225-25f80ef0a0d1 | replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419 | ||||||
|  |  | ||||||
| go 1.13 | go 1.17 | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import ( | |||||||
| 	"log" | 	"log" | ||||||
| 	"net" | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"regexp" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Message for rocketchat outgoing webhook. | // Message for rocketchat outgoing webhook. | ||||||
| @@ -68,7 +69,6 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 	} | 	} | ||||||
| 	msg := Message{} | 	msg := Message{} | ||||||
| 	body, err := ioutil.ReadAll(r.Body) | 	body, err := ioutil.ReadAll(r.Body) | ||||||
| 	log.Println(string(body)) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Println(err) | 		log.Println(err) | ||||||
| 		http.NotFound(w, r) | 		http.NotFound(w, r) | ||||||
| @@ -89,7 +89,11 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||
| 	msg.ChannelName = "#" + msg.ChannelName | 	msg.ChannelName = "#" + msg.ChannelName | ||||||
| 	if c.Token != "" { | 	if c.Token != "" { | ||||||
| 		if msg.Token != 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) | 			http.NotFound(w, r) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 270 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 170 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 282 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 204 KiB | 
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user