Compare commits
	
		
			187 Commits
		
	
	
		
			v1.23.1
			...
			updatexmpp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f73bee90ab | |||
| 186d28858b | |||
| bab3681ac2 | |||
| 
						
						
							
						
						b74b884793
	
				 | 
					
					
						|||
| 
						 | 
					c4157a4d5b | ||
| 
						 | 
					d16645c952 | ||
| 
						 | 
					65d78e38af | ||
| 
						 | 
					996e4a7fcf | ||
| 
						 | 
					a89267943c | ||
| 
						 | 
					815d8b804f | ||
| 
						 | 
					6edd5de3b7 | ||
| 
						 | 
					2e9db32a83 | ||
| 
						 | 
					4bf1c0450c | ||
| 
						 | 
					0bb521512a | ||
| 
						 | 
					13fd5c5d5b | ||
| 
						 | 
					d055b4530e | ||
| 
						 | 
					6b528ffa4f | ||
| 
						 | 
					fa147c076f | ||
| 
						 | 
					b2df32bc81 | ||
| 
						 | 
					733f4c71b8 | ||
| 
						 | 
					70e8c6e9d3 | ||
| 
						 | 
					2f33fe86f5 | ||
| 
						 | 
					56e7bd01ca | ||
| 
						 | 
					9459495484 | ||
| 
						 | 
					89b0d362d2 | ||
| 
						 | 
					574f25337d | ||
| 
						 | 
					5bbe422161 | ||
| 
						 | 
					6500714a93 | ||
| 
						 | 
					5feafcddba | ||
| 
						 | 
					3e20a3d180 | ||
| 
						 | 
					60639b6e50 | ||
| 
						 | 
					839f384e45 | ||
| 
						 | 
					d42277979a | ||
| 
						 | 
					24cf007a74 | ||
| 
						 | 
					601f48a50e | ||
| 
						 | 
					c2b8e298d8 | ||
| 
						 | 
					0917e17383 | ||
| 
						 | 
					8587fa8585 | ||
| 
						 | 
					f345eeae55 | ||
| 
						 | 
					89e2dbac15 | ||
| 
						 | 
					356ada872c | ||
| 
						 | 
					a3deb48726 | ||
| 
						 | 
					544cd5cd9e | ||
| 
						 | 
					53b63adc71 | ||
| 
						 | 
					2c349c50c7 | ||
| 
						 | 
					08779c2909 | ||
| 
						 | 
					d5f9cdf912 | ||
| 
						 | 
					3a4bdd7c56 | ||
| 
						 | 
					2b236f3ff7 | ||
| 
						 | 
					768fb791c5 | ||
| 
						 | 
					d00dcf3f58 | ||
| 
						 | 
					d5980303e5 | ||
| 
						 | 
					f436ae7b31 | ||
| 
						 | 
					a9ccc18d73 | ||
| 
						 | 
					ac681687f8 | ||
| 
						 | 
					57ce19150f | ||
| 
						 | 
					24f6747516 | ||
| 
						 | 
					6dafebc7cc | ||
| 
						 | 
					d23f2560d5 | ||
| 
						 | 
					880586bac4 | ||
| 
						 | 
					eac2a8c8dc | ||
| 
						 | 
					4cf313c4c6 | ||
| 
						 | 
					91353d0a4d | ||
| 
						 | 
					0a6d64ab48 | ||
| 
						 | 
					4fd0a76727 | ||
| 
						 | 
					6da9d567dc | ||
| 
						 | 
					6d5a3dff22 | ||
| 
						 | 
					3ad5deaff1 | ||
| 
						 | 
					9bbdf70e69 | ||
| 
						 | 
					0c83946983 | ||
| 
						 | 
					fda05f2262 | ||
| 
						 | 
					7abf1a5884 | ||
| 
						 | 
					365acc36ea | ||
| 
						 | 
					0482cd191d | ||
| 
						 | 
					6a3fc71397 | ||
| 
						 | 
					3c4192ebf6 | ||
| 
						 | 
					e450e1c447 | ||
| 
						 | 
					20f841c513 | ||
| 
						 | 
					d07a3e09c9 | ||
| 
						 | 
					4649876956 | ||
| 
						 | 
					5604d140e3 | ||
| 
						 | 
					8751fb4bb1 | ||
| 
						 | 
					3819062574 | ||
| 
						 | 
					051e6e76e9 | ||
| 
						 | 
					1e55dd47f2 | ||
| 
						 | 
					700b95546b | ||
| 
						 | 
					2fa96ec0ed | ||
| 
						 | 
					81e6f75aa4 | ||
| 
						 | 
					888c8b9a84 | ||
| 
						 | 
					e775a8a22e | ||
| 
						 | 
					99fbd9cae6 | ||
| 
						 | 
					67adad3e08 | ||
| 
						 | 
					2fca3c7563 | ||
| 
						 | 
					c3573f1a46 | ||
| 
						 | 
					ee932a9f8e | ||
| 
						 | 
					ce18c948e6 | ||
| 
						 | 
					7bc93c5506 | ||
| 
						 | 
					d7cad3b404 | ||
| 
						 | 
					7740a362c9 | ||
| 
						 | 
					281ef53e7d | ||
| 
						 | 
					f044b948e2 | ||
| 
						 | 
					32474a5f4d | ||
| 
						 | 
					26596acf80 | ||
| 
						 | 
					e63870a631 | ||
| 
						 | 
					ce782ff6fb | ||
| 
						 | 
					c6716e030c | ||
| 
						 | 
					4ab72acec6 | ||
| 
						 | 
					30aae8e257 | ||
| 
						 | 
					d7b7ff7bb4 | ||
| 
						 | 
					6fe0cff342 | ||
| 
						 | 
					5f75f9886d | ||
| 
						 | 
					5d9604cd15 | ||
| 
						 | 
					cc36ebf1c9 | ||
| 
						 | 
					e6adecfd81 | ||
| 
						 | 
					5c8f224e3b | ||
| 
						 | 
					952221d3b9 | ||
| 
						 | 
					496d5b4ec7 | ||
| 
						 | 
					2623a412c4 | ||
| 
						 | 
					d64eed49bc | ||
| 
						 | 
					fffa29c2f3 | ||
| 
						 | 
					4da1444ffc | ||
| 
						 | 
					21c4e56d16 | ||
| 
						 | 
					5356b3856a | ||
| 
						 | 
					320c996a21 | ||
| 
						 | 
					69c74be7bb | ||
| 
						 | 
					aefa70891c | ||
| 
						 | 
					1b9877fda4 | ||
| 
						 | 
					0205a67309 | ||
| 
						 | 
					e3cafeaf92 | ||
| 
						 | 
					e7b193788a | ||
| 
						 | 
					17da95b094 | ||
| 
						 | 
					c5e49eec96 | ||
| 
						 | 
					24bc0f127b | ||
| 
						 | 
					f0f801402d | ||
| 
						 | 
					663850a2b8 | ||
| 
						 | 
					c51753cab1 | ||
| 
						 | 
					b3be2e208c | ||
| 
						 | 
					c30e90ff3f | ||
| 
						 | 
					e4c0ca0f48 | ||
| 
						 | 
					9c203327c0 | ||
| 
						 | 
					ccb5b1d075 | ||
| 
						 | 
					0dbbd0414c | ||
| 
						 | 
					e7b3ebf98a | ||
| 
						 | 
					5bc18fb780 | ||
| 
						 | 
					df30366072 | ||
| 
						 | 
					65c7ac80b5 | ||
| 
						 | 
					dd3fb32ec7 | ||
| 
						 | 
					2a3f475ff5 | ||
| 
						 | 
					7288f71201 | ||
| 
						 | 
					9c43eff753 | ||
| 
						 | 
					c8d7fdeedc | ||
| 
						 | 
					c211152e23 | ||
| 
						 | 
					ab75d5097e | ||
| 
						 | 
					c3644c8d3b | ||
| 
						 | 
					6438a3dba3 | ||
| 
						 | 
					4b226a6a63 | ||
| 
						 | 
					4801850013 | ||
| 
						 | 
					6a7412bf2b | ||
| 
						 | 
					5a1fd7dadd | ||
| 
						 | 
					ac06a26809 | ||
| 
						 | 
					61d56f26f8 | ||
| 
						 | 
					6aa05b3981 | ||
| 
						 | 
					aad60c882e | ||
| 
						 | 
					fecca57507 | ||
| 
						 | 
					2bcad846c0 | ||
| 
						 | 
					15ad0165fc | ||
| 
						 | 
					2e8ab11978 | ||
| 
						 | 
					9a8ce9b17e | ||
| 
						 | 
					16ab4c6fed | ||
| 
						 | 
					e3ee0df7ba | ||
| 
						 | 
					8f7ab280e2 | ||
| 
						 | 
					dbedc99421 | ||
| 
						 | 
					6cb359cb80 | ||
| 
						 | 
					ae2ad824a9 | ||
| 
						 | 
					02e3d7852b | ||
| 
						 | 
					3893a035be | ||
| 
						 | 
					658bdd9faa | ||
| 
						 | 
					e1eebcd4e0 | ||
| 
						 | 
					062b831e88 | ||
| 
						 | 
					b275efaeff | ||
| 
						 | 
					80d3033456 | ||
| 
						 | 
					bd0516f09a | ||
| 
						 | 
					df4d76e466 | ||
| 
						 | 
					dcbd7f8cad | ||
| 
						 | 
					73ec02ab9d | ||
| 
						 | 
					d1f8347071 | ||
| 
						 | 
					8601eedada | 
							
								
								
									
										24
									
								
								.github/workflows/development.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/development.yml
									
									
									
									
										vendored
									
									
								
							@@ -5,28 +5,28 @@ jobs:
 | 
			
		||||
    name: golangci-lint
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 20
 | 
			
		||||
      - name: Run golangci-lint
 | 
			
		||||
        uses: golangci/golangci-lint-action@v2
 | 
			
		||||
        uses: golangci/golangci-lint-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          version: latest
 | 
			
		||||
          args: "-v --new-from-rev HEAD~5"
 | 
			
		||||
          args: "-v --new-from-rev HEAD~5 --timeout=5m"
 | 
			
		||||
  test-build-upload:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        go-version: [1.17.x]
 | 
			
		||||
        go-version: [1.22.x]
 | 
			
		||||
        platform: [ubuntu-latest]
 | 
			
		||||
    runs-on: ${{ matrix.platform }}
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Install Go
 | 
			
		||||
      uses: actions/setup-go@v2
 | 
			
		||||
      uses: actions/setup-go@v3
 | 
			
		||||
      with:
 | 
			
		||||
        go-version: ${{ matrix.go-version }}
 | 
			
		||||
        stable: false
 | 
			
		||||
    - name: Checkout code
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
      uses: actions/checkout@v3
 | 
			
		||||
      with:
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
    - name: Test
 | 
			
		||||
@@ -39,20 +39,20 @@ jobs:
 | 
			
		||||
        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
 | 
			
		||||
      if: startsWith(matrix.go-version,'1.22')
 | 
			
		||||
      uses: actions/upload-artifact@v3
 | 
			
		||||
      with:
 | 
			
		||||
        name: matterbridge-linux-64bit
 | 
			
		||||
        path: output/lin
 | 
			
		||||
    - name: Upload windows 64-bit
 | 
			
		||||
      if: startsWith(matrix.go-version,'1.17')
 | 
			
		||||
      uses: actions/upload-artifact@v2
 | 
			
		||||
      if: startsWith(matrix.go-version,'1.22')
 | 
			
		||||
      uses: actions/upload-artifact@v3
 | 
			
		||||
      with:
 | 
			
		||||
        name: matterbridge-windows-64bit
 | 
			
		||||
        path: output/win
 | 
			
		||||
    - name: Upload darwin 64-bit
 | 
			
		||||
      if: startsWith(matrix.go-version,'1.17')
 | 
			
		||||
      uses: actions/upload-artifact@v2
 | 
			
		||||
      if: startsWith(matrix.go-version,'1.22')
 | 
			
		||||
      uses: actions/upload-artifact@v3
 | 
			
		||||
      with:
 | 
			
		||||
        name: matterbridge-darwin-64bit
 | 
			
		||||
        path: output/mac
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -4,3 +4,6 @@
 | 
			
		||||
 | 
			
		||||
# Exclude configuration file
 | 
			
		||||
matterbridge.toml
 | 
			
		||||
 | 
			
		||||
# Exclude IDE Files
 | 
			
		||||
.vscode
 | 
			
		||||
 
 | 
			
		||||
@@ -188,7 +188,33 @@ linters:
 | 
			
		||||
    - exhaustivestruct
 | 
			
		||||
    - forbidigo
 | 
			
		||||
    - wrapcheck
 | 
			
		||||
 | 
			
		||||
    - varnamelen
 | 
			
		||||
    - ireturn
 | 
			
		||||
    - errorlint
 | 
			
		||||
    - tparallel
 | 
			
		||||
    - wrapcheck
 | 
			
		||||
    - paralleltest
 | 
			
		||||
    - makezero
 | 
			
		||||
    - thelper
 | 
			
		||||
    - cyclop
 | 
			
		||||
    - revive
 | 
			
		||||
    - importas
 | 
			
		||||
    - gomoddirectives
 | 
			
		||||
    - promlinter
 | 
			
		||||
    - tagliatelle
 | 
			
		||||
    - errname
 | 
			
		||||
    - typecheck
 | 
			
		||||
    - grouper
 | 
			
		||||
    - decorder
 | 
			
		||||
    - maintidx
 | 
			
		||||
    - exhaustruct
 | 
			
		||||
    - asasalint
 | 
			
		||||
    - execinquery
 | 
			
		||||
    - nosnakecase
 | 
			
		||||
    - exhaustive
 | 
			
		||||
    - testifylint
 | 
			
		||||
    - mnd
 | 
			
		||||
    - depguard
 | 
			
		||||
# rules to deal with reported isues
 | 
			
		||||
issues:
 | 
			
		||||
  # List of regexps of issue texts to exclude, empty list by default.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM alpine:edge AS builder
 | 
			
		||||
FROM alpine AS builder
 | 
			
		||||
 | 
			
		||||
COPY . /go/src/matterbridge
 | 
			
		||||
RUN apk --no-cache add go git \
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								Dockerfile_whatsappmulti
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Dockerfile_whatsappmulti
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
FROM alpine AS builder
 | 
			
		||||
 | 
			
		||||
COPY . /go/src/matterbridge
 | 
			
		||||
RUN apk --no-cache add go git \
 | 
			
		||||
        && cd /go/src/matterbridge \
 | 
			
		||||
        && CGO_ENABLED=0 go build -tags whatsappmulti -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
 | 
			
		||||
 | 
			
		||||
FROM alpine
 | 
			
		||||
RUN apk --no-cache add ca-certificates mailcap
 | 
			
		||||
COPY --from=builder /bin/matterbridge /bin/matterbridge
 | 
			
		||||
RUN mkdir /etc/matterbridge \
 | 
			
		||||
  && touch /etc/matterbridge/matterbridge.toml \
 | 
			
		||||
  && ln -sf /matterbridge.toml /etc/matterbridge/matterbridge.toml
 | 
			
		||||
ENTRYPOINT ["/bin/matterbridge", "-conf", "/etc/matterbridge/matterbridge.toml"]
 | 
			
		||||
							
								
								
									
										91
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										91
									
								
								README.md
									
									
									
									
									
								
							@@ -58,21 +58,22 @@ And more...
 | 
			
		||||
    - [Binaries](#binaries)
 | 
			
		||||
    - [Packages](#packages)
 | 
			
		||||
  - [Building](#building)
 | 
			
		||||
  - [Building with whatsapp (beta) multidevice support](#building-with-whatsapp-beta-multidevice-support)
 | 
			
		||||
  - [Configuration](#configuration)
 | 
			
		||||
    - [Basic configuration](#basic-configuration)
 | 
			
		||||
    - [Settings](#settings)
 | 
			
		||||
    - [Advanced configuration](#advanced-configuration)
 | 
			
		||||
    - [Examples](#examples)
 | 
			
		||||
      - [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing)
 | 
			
		||||
      - [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general)
 | 
			
		||||
  - [Running](#running)
 | 
			
		||||
    - [Docker](#docker)
 | 
			
		||||
    - [Systemd](#systemd)
 | 
			
		||||
  - [Changelog](#changelog)
 | 
			
		||||
  - [FAQ](#faq)
 | 
			
		||||
  - [Related projects](#related-projects)
 | 
			
		||||
  - [Articles / Tutorials](#articles--tutorials)
 | 
			
		||||
  - [Thanks](#thanks)
 | 
			
		||||
      - [Examples](#examples)
 | 
			
		||||
        - [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing)
 | 
			
		||||
        - [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general)
 | 
			
		||||
    - [Running](#running)
 | 
			
		||||
      - [Docker](#docker)
 | 
			
		||||
      - [Systemd](#systemd)
 | 
			
		||||
    - [Changelog](#changelog)
 | 
			
		||||
    - [FAQ](#faq)
 | 
			
		||||
    - [Related projects](#related-projects)
 | 
			
		||||
    - [Articles / Tutorials](#articles--tutorials)
 | 
			
		||||
    - [Thanks](#thanks)
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
@@ -89,6 +90,7 @@ And more...
 | 
			
		||||
 | 
			
		||||
- [Discord](https://discordapp.com)
 | 
			
		||||
- [Gitter](https://gitter.im)
 | 
			
		||||
- [Harmony](https://harmonyapp.io)
 | 
			
		||||
- [IRC](http://www.mirc.com/servers.html)
 | 
			
		||||
- [Keybase](https://keybase.io)
 | 
			
		||||
- [Matrix](https://matrix.org)
 | 
			
		||||
@@ -105,21 +107,28 @@ And more...
 | 
			
		||||
- [Twitch](https://twitch.tv)
 | 
			
		||||
- [VK](https://vk.com/)
 | 
			
		||||
- [WhatsApp](https://www.whatsapp.com/)
 | 
			
		||||
  - Whatsapp legacy is natively supported
 | 
			
		||||
  - Whatsapp multidevice beta is natively supported but you need to build yourself, see [here](#building-with-whatsapp-beta-multidevice-support)
 | 
			
		||||
- [XMPP](https://xmpp.org)
 | 
			
		||||
- [Zulip](https://zulipchat.com)
 | 
			
		||||
 | 
			
		||||
### 3rd party via matterbridge api
 | 
			
		||||
 | 
			
		||||
- [Delta Chat](https://github.com/deltachat-bot/matterdelta)
 | 
			
		||||
- [Minecraft](https://github.com/raws/mattercraft)
 | 
			
		||||
- [Minecraft](https://gitlab.com/Programie/MatterBukkit)
 | 
			
		||||
 | 
			
		||||
#### Past 3rd party projects
 | 
			
		||||
- [Discourse](https://github.com/DeclanHoare/matterbabble)
 | 
			
		||||
- [Facebook messenger](https://github.com/powerjungle/fbridge-asyncio)
 | 
			
		||||
- [Facebook messenger](https://github.com/VictorNine/fbridge)
 | 
			
		||||
- [Minecraft](https://github.com/elytra/MatterLink)
 | 
			
		||||
- [Minecraft](https://github.com/raws/mattercraft)
 | 
			
		||||
- [Minecraft](https://gitlab.com/Programie/MatterBukkit)
 | 
			
		||||
- [Reddit](https://github.com/bonehurtingjuice/mattereddit)
 | 
			
		||||
- [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
 | 
			
		||||
- [MatterAMXX](https://github.com/GabeIggy/MatterAMXX)
 | 
			
		||||
- [MatterAMXX](https://github.com/andrewlindberg/MatterAMXX): [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
 | 
			
		||||
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
 | 
			
		||||
- [Ultima Online Emulator](https://github.com/kuoushi/ServUO-Matterbridge)
 | 
			
		||||
- [Teamspeak](https://github.com/Archeb/ts-matterbridge)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### API
 | 
			
		||||
 | 
			
		||||
@@ -138,6 +147,9 @@ Used by the projects below. Feel free to make a PR to add your project to this l
 | 
			
		||||
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
 | 
			
		||||
- [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod)
 | 
			
		||||
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
 | 
			
		||||
- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
 | 
			
		||||
- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
 | 
			
		||||
- [beerchat](https://github.com/mt-mods/beerchat) (Matterbridge link for minetest)
 | 
			
		||||
 | 
			
		||||
## Chat with us
 | 
			
		||||
 | 
			
		||||
@@ -164,10 +176,10 @@ See <https://github.com/42wim/matterbridge/wiki>
 | 
			
		||||
 | 
			
		||||
### Binaries
 | 
			
		||||
 | 
			
		||||
- Latest stable release [v1.23.1](https://github.com/42wim/matterbridge/releases/latest)
 | 
			
		||||
- Latest stable release [v1.26.0](https://github.com/42wim/matterbridge/releases/latest)
 | 
			
		||||
- Development releases (follows master) can be downloaded [here](https://github.com/42wim/matterbridge/actions) selecting the latest green build and then artifacts.
 | 
			
		||||
 | 
			
		||||
To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest). On \*nix platforms you may need to make the binary executable - you can do this by running `chmod a+x` on the binary (example: `chmod a+x matterbridge-1.20.0-linux-64bit`). After downloading (and making the binary executable, if necessary), follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
 | 
			
		||||
To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest). On \*nix platforms you may need to make the binary executable - you can do this by running `chmod a+x` on the binary (example: `chmod a+x matterbridge-1.24.1-linux-64bit`). After downloading (and making the binary executable, if necessary), follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
 | 
			
		||||
 | 
			
		||||
### Packages
 | 
			
		||||
 | 
			
		||||
@@ -180,7 +192,12 @@ To install or upgrade just download the latest [binary](https://github.com/42wim
 | 
			
		||||
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest)
 | 
			
		||||
 | 
			
		||||
If you really want to build from source, follow these instructions:
 | 
			
		||||
Go 1.17+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed.
 | 
			
		||||
Go 1.18+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed.
 | 
			
		||||
 | 
			
		||||
Building the binary with **all** the bridges enabled needs about 3GB RAM to compile.
 | 
			
		||||
You can reduce this memory requirement to 0,5GB RAM by adding the `nomsteams` tag if you don't need/use the Microsoft Teams bridge.
 | 
			
		||||
 | 
			
		||||
Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`)
 | 
			
		||||
 | 
			
		||||
To install the latest stable run:
 | 
			
		||||
 | 
			
		||||
@@ -194,6 +211,38 @@ To install the latest dev run:
 | 
			
		||||
go install github.com/42wim/matterbridge@master
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
To install the latest stable run without msteams or zulip bridge:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
go install -tags nomsteams,nozulip github.com/42wim/matterbridge
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You should now have matterbridge binary in the ~/go/bin directory:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
$ ls ~/go/bin/
 | 
			
		||||
matterbridge
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Building with whatsapp (beta) multidevice support
 | 
			
		||||
 | 
			
		||||
Because the library we use for Whatsapp multidevice support includes a GPL3 library we can not provide you binaries.
 | 
			
		||||
(as this would require the Matterbridge to change it license to GPL)
 | 
			
		||||
 | 
			
		||||
Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`)
 | 
			
		||||
 | 
			
		||||
So this means you have to build it yourself using the instructions below:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
go install -tags whatsappmulti github.com/42wim/matterbridge@master
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If you're low on memory and don't need msteams:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
go install -tags nomsteams,whatsappmulti github.com/42wim/matterbridge@master
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You should now have matterbridge binary in the ~/go/bin directory:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
@@ -323,6 +372,8 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
 | 
			
		||||
- [nextcloud talk](https://github.com/nextcloud/talk_matterbridge) (Integrates matterbridge in Nextcloud Talk)
 | 
			
		||||
- [mattercraft](https://github.com/raws/mattercraft) (Minecraft bridge)
 | 
			
		||||
- [vs-matterbridge](https://github.com/NikkyAI/vs-matterbridge) (Vintage Story bridge)
 | 
			
		||||
- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
 | 
			
		||||
- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
 | 
			
		||||
 | 
			
		||||
## Articles / Tutorials
 | 
			
		||||
 | 
			
		||||
@@ -352,10 +403,10 @@ Matterbridge wouldn't exist without these libraries:
 | 
			
		||||
 | 
			
		||||
- discord - <https://github.com/bwmarrin/discordgo>
 | 
			
		||||
- echo - <https://github.com/labstack/echo>
 | 
			
		||||
- gitter - <https://github.com/sromku/go-gitter>
 | 
			
		||||
- gops - <https://github.com/google/gops>
 | 
			
		||||
- gozulipbot - <https://github.com/ifo/gozulipbot>
 | 
			
		||||
- gumble - <https://github.com/layeh/gumble>
 | 
			
		||||
- harmony - <https://github.com/harmony-development/shibshib>
 | 
			
		||||
- irc - <https://github.com/lrstanley/girc>
 | 
			
		||||
- keybase - <https://github.com/keybase/go-keybase-chat-bot>
 | 
			
		||||
- matrix - <https://github.com/matrix-org/gomatrix>
 | 
			
		||||
@@ -363,6 +414,7 @@ Matterbridge wouldn't exist without these libraries:
 | 
			
		||||
- msgraph.go - <https://github.com/yaegashi/msgraph.go>
 | 
			
		||||
- mumble - <https://github.com/layeh/gumble>
 | 
			
		||||
- nctalk - <https://github.com/gary-kim/go-nc-talk>
 | 
			
		||||
- rocketchat - <https://github.com/RocketChat/Rocket.Chat.Go.SDK>
 | 
			
		||||
- slack - <https://github.com/nlopes/slack>
 | 
			
		||||
- sshchat - <https://github.com/shazow/ssh-chat>
 | 
			
		||||
- steam - <https://github.com/Philipp15b/go-steam>
 | 
			
		||||
@@ -370,6 +422,7 @@ Matterbridge wouldn't exist without these libraries:
 | 
			
		||||
- tengo - <https://github.com/d5/tengo>
 | 
			
		||||
- vk - <https://github.com/SevereCloud/vksdk>
 | 
			
		||||
- whatsapp - <https://github.com/Rhymen/go-whatsapp>
 | 
			
		||||
- whatsapp - <https://github.com/tulir/whatsmeow>
 | 
			
		||||
- xmpp - <https://github.com/mattn/go-xmpp>
 | 
			
		||||
- zulip - <https://github.com/ifo/gozulipbot>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,20 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"gopkg.in/olahol/melody.v1"
 | 
			
		||||
	"github.com/olahol/melody"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"github.com/labstack/echo/v4/middleware"
 | 
			
		||||
	"github.com/mitchellh/mapstructure"
 | 
			
		||||
	ring "github.com/zfjagann/golang-ring"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -137,6 +140,36 @@ func (b *API) handlePostMessage(c echo.Context) error {
 | 
			
		||||
	message.Account = b.Account
 | 
			
		||||
	message.ID = ""
 | 
			
		||||
	message.Timestamp = time.Now()
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		fm map[string]interface{}
 | 
			
		||||
		ds string
 | 
			
		||||
		ok bool
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	for i, f := range message.Extra["file"] {
 | 
			
		||||
		fi := config.FileInfo{}
 | 
			
		||||
		if fm, ok = f.(map[string]interface{}); !ok {
 | 
			
		||||
			return echo.NewHTTPError(http.StatusInternalServerError, "invalid format for extra")
 | 
			
		||||
		}
 | 
			
		||||
		err := mapstructure.Decode(fm, &fi)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if !strings.Contains(err.Error(), "got string") {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// mapstructure doesn't decode base64 into []byte, so it must be done manually for fi.Data
 | 
			
		||||
		if ds, ok = fm["Data"].(string); !ok {
 | 
			
		||||
			return echo.NewHTTPError(http.StatusInternalServerError, "invalid format for data")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data, err := base64.StdEncoding.DecodeString(ds)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		fi.Data = &data
 | 
			
		||||
		message.Extra["file"][i] = fi
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
 | 
			
		||||
	b.Remote <- message
 | 
			
		||||
	return c.JSON(http.StatusOK, message)
 | 
			
		||||
@@ -166,15 +199,20 @@ func (b *API) handleStream(c echo.Context) error {
 | 
			
		||||
	}
 | 
			
		||||
	c.Response().Flush()
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		// TODO: this causes issues, messages should be broadcasted to all connected clients
 | 
			
		||||
		msg := b.Messages.Dequeue()
 | 
			
		||||
		if msg != nil {
 | 
			
		||||
			if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
		default:
 | 
			
		||||
			msg := b.Messages.Dequeue()
 | 
			
		||||
			if msg != nil {
 | 
			
		||||
				if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				c.Response().Flush()
 | 
			
		||||
			}
 | 
			
		||||
			c.Response().Flush()
 | 
			
		||||
			time.Sleep(100 * time.Millisecond)
 | 
			
		||||
		case <-c.Request().Context().Done():
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		time.Sleep(200 * time.Millisecond)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ const (
 | 
			
		||||
	EventRejoinChannels    = "rejoin_channels"
 | 
			
		||||
	EventUserAction        = "user_action"
 | 
			
		||||
	EventMsgDelete         = "msg_delete"
 | 
			
		||||
	EventFileDelete        = "file_delete"
 | 
			
		||||
	EventAPIConnected      = "api_connected"
 | 
			
		||||
	EventUserTyping        = "user_typing"
 | 
			
		||||
	EventGetChannelMembers = "get_channel_members"
 | 
			
		||||
@@ -56,13 +57,14 @@ func (m Message) ParentValid() bool {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FileInfo struct {
 | 
			
		||||
	Name    string
 | 
			
		||||
	Data    *[]byte
 | 
			
		||||
	Comment string
 | 
			
		||||
	URL     string
 | 
			
		||||
	Size    int64
 | 
			
		||||
	Avatar  bool
 | 
			
		||||
	SHA     string
 | 
			
		||||
	Name     string
 | 
			
		||||
	Data     *[]byte
 | 
			
		||||
	Comment  string
 | 
			
		||||
	URL      string
 | 
			
		||||
	Size     int64
 | 
			
		||||
	Avatar   bool
 | 
			
		||||
	SHA      string
 | 
			
		||||
	NativeID string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChannelInfo struct {
 | 
			
		||||
@@ -119,6 +121,7 @@ type Protocol struct {
 | 
			
		||||
	MessageLength          int        // IRC, max length of a message allowed
 | 
			
		||||
	MessageQueue           int        // IRC, size of message queue for flood control
 | 
			
		||||
	MessageSplit           bool       // IRC, split long messages with newlines on MessageLength instead of clipping
 | 
			
		||||
	MessageSplitMaxCount   int        // discord, split long messages into at most this many messages instead of clipping (MessageLength=1950 cannot be configured)
 | 
			
		||||
	Muc                    string     // xmpp
 | 
			
		||||
	MxID                   string     // matrix
 | 
			
		||||
	Name                   string     // all protocols
 | 
			
		||||
@@ -168,7 +171,7 @@ type Protocol struct {
 | 
			
		||||
	UseTLS                 bool       // IRC
 | 
			
		||||
	UseDiscriminator       bool       // discord
 | 
			
		||||
	UseFirstName           bool       // telegram
 | 
			
		||||
	UseUserName            bool       // discord, matrix
 | 
			
		||||
	UseUserName            bool       // discord, matrix, mattermost
 | 
			
		||||
	UseInsecureURL         bool       // telegram
 | 
			
		||||
	UserName               string     // IRC
 | 
			
		||||
	VerboseJoinPart        bool       // IRC
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,14 @@ import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/discord/transmitter"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/matterbridge/discordgo"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
	lru "github.com/hashicorp/golang-lru"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const MessageLength = 1950
 | 
			
		||||
const (
 | 
			
		||||
	MessageLength = 1950
 | 
			
		||||
	cFileUpload   = "file_upload"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Bdiscord struct {
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
@@ -35,10 +39,20 @@ type Bdiscord struct {
 | 
			
		||||
	// Webhook specific logic
 | 
			
		||||
	useAutoWebhooks bool
 | 
			
		||||
	transmitter     *transmitter.Transmitter
 | 
			
		||||
	cache           *lru.Cache
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	b := &Bdiscord{Config: cfg}
 | 
			
		||||
	newCache, err := lru.New(5000)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		cfg.Log.Fatalf("Could not create LRU cache: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b := &Bdiscord{
 | 
			
		||||
		Config: cfg,
 | 
			
		||||
		cache:  newCache,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.userMemberMap = make(map[string]*discordgo.Member)
 | 
			
		||||
	b.nickMemberMap = make(map[string]*discordgo.Member)
 | 
			
		||||
	b.channelInfoMap = make(map[string]*config.ChannelInfo)
 | 
			
		||||
@@ -67,14 +81,6 @@ func (b *Bdiscord) Connect() error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	b.c.AddHandler(b.messageCreate)
 | 
			
		||||
	b.c.AddHandler(b.messageTyping)
 | 
			
		||||
	b.c.AddHandler(b.memberUpdate)
 | 
			
		||||
	b.c.AddHandler(b.messageUpdate)
 | 
			
		||||
	b.c.AddHandler(b.messageDelete)
 | 
			
		||||
	b.c.AddHandler(b.messageDeleteBulk)
 | 
			
		||||
	b.c.AddHandler(b.memberAdd)
 | 
			
		||||
	b.c.AddHandler(b.memberRemove)
 | 
			
		||||
	// Add privileged intent for guild member tracking. This is needed to track nicks
 | 
			
		||||
	// for display names and @mention translation
 | 
			
		||||
	b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
 | 
			
		||||
@@ -84,7 +90,7 @@ func (b *Bdiscord) Connect() error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	guilds, err := b.c.UserGuilds(100, "", "")
 | 
			
		||||
	guilds, err := b.c.UserGuilds(100, "", "", false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -153,7 +159,7 @@ func (b *Bdiscord) Connect() error {
 | 
			
		||||
		return fmt.Errorf("use of removed WebhookURL setting")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.GetInt("debuglevel") > 0 {
 | 
			
		||||
	if b.GetInt("debuglevel") == 2 {
 | 
			
		||||
		b.Log.Debug("enabling even more discord debug")
 | 
			
		||||
		b.c.Debug = true
 | 
			
		||||
	}
 | 
			
		||||
@@ -216,6 +222,19 @@ func (b *Bdiscord) Connect() error {
 | 
			
		||||
			b.nickMemberMap[member.Nick] = member
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.c.AddHandler(b.messageCreate)
 | 
			
		||||
	b.c.AddHandler(b.messageTyping)
 | 
			
		||||
	b.c.AddHandler(b.messageUpdate)
 | 
			
		||||
	b.c.AddHandler(b.messageDelete)
 | 
			
		||||
	b.c.AddHandler(b.messageDeleteBulk)
 | 
			
		||||
	b.c.AddHandler(b.memberAdd)
 | 
			
		||||
	b.c.AddHandler(b.memberRemove)
 | 
			
		||||
	b.c.AddHandler(b.memberUpdate)
 | 
			
		||||
	if b.GetInt("debuglevel") == 1 {
 | 
			
		||||
		b.c.AddHandler(b.messageEvent)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -255,7 +274,6 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
 | 
			
		||||
	// Handle prefix hint for unthreaded messages.
 | 
			
		||||
	if msg.ParentNotFound() {
 | 
			
		||||
		msg.ParentID = ""
 | 
			
		||||
		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Use webhook to send the message
 | 
			
		||||
@@ -280,9 +298,25 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Delete a file
 | 
			
		||||
	if msg.Event == config.EventFileDelete {
 | 
			
		||||
		if msg.ID == "" {
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if fi, ok := b.cache.Get(cFileUpload + msg.ID); ok {
 | 
			
		||||
			err := b.c.ChannelMessageDelete(channelID, fi.(string)) // nolint:forcetypeassert
 | 
			
		||||
			b.cache.Remove(cFileUpload + msg.ID)
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return "", fmt.Errorf("file %s not found", msg.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Upload a file if it exists
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		for _, rmsg := range helper.HandleExtra(msg, b.General) {
 | 
			
		||||
			// TODO: Use ClipOrSplitMessage
 | 
			
		||||
			rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped"))
 | 
			
		||||
			if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
 | 
			
		||||
				b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
 | 
			
		||||
@@ -294,40 +328,65 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
 | 
			
		||||
	msg.Text = b.replaceUserMentions(msg.Text)
 | 
			
		||||
 | 
			
		||||
	// Edit message
 | 
			
		||||
	if msg.ID != "" {
 | 
			
		||||
		_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
 | 
			
		||||
		return msg.ID, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m := discordgo.MessageSend{
 | 
			
		||||
		Content:         msg.Username + msg.Text,
 | 
			
		||||
		AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.ParentValid() {
 | 
			
		||||
		m.Reference = &discordgo.MessageReference{
 | 
			
		||||
			MessageID: msg.ParentID,
 | 
			
		||||
			ChannelID: channelID,
 | 
			
		||||
			GuildID:   b.guildID,
 | 
			
		||||
		// Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";".
 | 
			
		||||
		msgIds := strings.Split(msg.ID, ";")
 | 
			
		||||
		msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds))
 | 
			
		||||
		for len(msgParts) < len(msgIds) {
 | 
			
		||||
			msgParts = append(msgParts, "((obsoleted by edit))")
 | 
			
		||||
		}
 | 
			
		||||
		for i := range msgParts {
 | 
			
		||||
			// In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates.
 | 
			
		||||
			// TODO: Optimize away noop-updates of un-edited messages
 | 
			
		||||
			// TODO: Use RemoteNickFormat instead of this broken concatenation
 | 
			
		||||
			_, err := b.c.ChannelMessageEdit(channelID, msgIds[i], msg.Username+msgParts[i])
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return "", err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return msg.ID, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post normal message
 | 
			
		||||
	res, err := b.c.ChannelMessageSendComplex(channelID, &m)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), b.GetInt("MessageSplitMaxCount"))
 | 
			
		||||
	msgIds := []string{}
 | 
			
		||||
 | 
			
		||||
	for _, msgPart := range msgParts {
 | 
			
		||||
		m := discordgo.MessageSend{
 | 
			
		||||
			Embeds: []*discordgo.MessageEmbed{
 | 
			
		||||
				{
 | 
			
		||||
					Description: msgPart,
 | 
			
		||||
					Author: &discordgo.MessageEmbedAuthor{
 | 
			
		||||
						Name: msg.Username,
 | 
			
		||||
						IconURL: msg.Avatar,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if msg.ParentValid() {
 | 
			
		||||
			m.Reference = &discordgo.MessageReference{
 | 
			
		||||
				MessageID: msg.ParentID,
 | 
			
		||||
				ChannelID: channelID,
 | 
			
		||||
				GuildID:   b.guildID,
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Post normal message
 | 
			
		||||
		res, err := b.c.ChannelMessageSendComplex(channelID, &m)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		msgIds = append(msgIds, res.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res.ID, nil
 | 
			
		||||
	// Exploit that a discord message ID is actually just a large number, so we encode a list of IDs by separating them with ";".
 | 
			
		||||
	return strings.Join(msgIds, ";"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleUploadFile handles native upload of files
 | 
			
		||||
func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
		file := discordgo.File{
 | 
			
		||||
@@ -340,10 +399,15 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
 | 
			
		||||
			Files:           []*discordgo.File{&file},
 | 
			
		||||
			AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
		}
 | 
			
		||||
		_, err = b.c.ChannelMessageSendComplex(channelID, &m)
 | 
			
		||||
		res, err := b.c.ChannelMessageSendComplex(channelID, &m)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", fmt.Errorf("file upload failed: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// link file_upload_nativeID (file ID from the original bridge) to our upload id
 | 
			
		||||
		// so that we can remove this later when it eg needs to be deleted
 | 
			
		||||
		b.cache.Add(cFileUpload+fi.NativeID, res.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,15 @@ package bdiscord
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/matterbridge/discordgo"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
	"github.com/davecgh/go-spew/spew"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam
 | 
			
		||||
	if m.GuildID != b.guildID {
 | 
			
		||||
		b.Log.Debugf("Ignoring messageDelete because it originates from a different guild")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
 | 
			
		||||
	rmsg.Channel = b.getChannelName(m.ChannelID)
 | 
			
		||||
 | 
			
		||||
@@ -16,6 +21,10 @@ func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelet
 | 
			
		||||
 | 
			
		||||
// TODO(qaisjp): if other bridges support bulk deletions, it could be fanned out centrally
 | 
			
		||||
func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageDeleteBulk) { //nolint:unparam
 | 
			
		||||
	if m.GuildID != b.guildID {
 | 
			
		||||
		b.Log.Debugf("Ignoring messageDeleteBulk because it originates from a different guild")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	for _, msgID := range m.Messages {
 | 
			
		||||
		rmsg := config.Message{
 | 
			
		||||
			Account: b.Account,
 | 
			
		||||
@@ -31,7 +40,15 @@ func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageD
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) messageEvent(s *discordgo.Session, m *discordgo.Event) {
 | 
			
		||||
	b.Log.Debug(spew.Sdump(m.Struct))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) {
 | 
			
		||||
	if m.GuildID != b.guildID {
 | 
			
		||||
		b.Log.Debugf("Ignoring messageTyping because it originates from a different guild")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if !b.GetBool("ShowUserTyping") {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -47,11 +64,15 @@ func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam
 | 
			
		||||
	if m.GuildID != b.guildID {
 | 
			
		||||
		b.Log.Debugf("Ignoring messageUpdate because it originates from a different guild")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if b.GetBool("EditDisable") {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// only when message is actually edited
 | 
			
		||||
	if m.Message.EditedTimestamp != "" {
 | 
			
		||||
	if m.Message.EditedTimestamp != nil {
 | 
			
		||||
		b.Log.Debugf("Sending edit message")
 | 
			
		||||
		m.Content += b.GetString("EditSuffix")
 | 
			
		||||
		msg := &discordgo.MessageCreate{
 | 
			
		||||
@@ -62,6 +83,10 @@ func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdat
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam
 | 
			
		||||
	if m.GuildID != b.guildID {
 | 
			
		||||
		b.Log.Debugf("Ignoring messageCreate because it originates from a different guild")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	// not relay our own messages
 | 
			
		||||
@@ -82,8 +107,9 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("== Receiving event %#v", m.Message)
 | 
			
		||||
 | 
			
		||||
	if m.Content != "" {
 | 
			
		||||
		b.Log.Debugf("== Receiving event %#v", m.Message)
 | 
			
		||||
		m.Message.Content = b.replaceChannelMentions(m.Message.Content)
 | 
			
		||||
		rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -138,6 +164,10 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
 | 
			
		||||
	if m.GuildID != b.guildID {
 | 
			
		||||
		b.Log.Debugf("Ignoring memberUpdate because it originates from a different guild")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if m.Member == nil {
 | 
			
		||||
		b.Log.Warnf("Received member update with no member information: %#v", m)
 | 
			
		||||
	}
 | 
			
		||||
@@ -165,6 +195,13 @@ func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
 | 
			
		||||
	if m.GuildID != b.guildID {
 | 
			
		||||
		b.Log.Debugf("Ignoring memberAdd because it originates from a different guild")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if b.GetBool("nosendjoinpart") {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if m.Member == nil {
 | 
			
		||||
		b.Log.Warnf("Received member update with no member information: %#v", m)
 | 
			
		||||
		return
 | 
			
		||||
@@ -186,6 +223,13 @@ func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
 | 
			
		||||
	if m.GuildID != b.guildID {
 | 
			
		||||
		b.Log.Debugf("Ignoring memberRemove because it originates from a different guild")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if b.GetBool("nosendjoinpart") {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if m.Member == nil {
 | 
			
		||||
		b.Log.Warnf("Received member update with no member information: %#v", m)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ package bdiscord
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/matterbridge/discordgo"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unicode"
 | 
			
		||||
 | 
			
		||||
	"github.com/matterbridge/discordgo"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) getAllowedMentions() *discordgo.MessageAllowedMentions {
 | 
			
		||||
@@ -68,7 +68,7 @@ func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error)
 | 
			
		||||
	b.membersMutex.RLock()
 | 
			
		||||
	defer b.membersMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	if member, ok := b.nickMemberMap[nick]; ok {
 | 
			
		||||
	if member, ok := b.nickMemberMap[strings.TrimSpace(nick)]; ok {
 | 
			
		||||
		return member, nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ import (
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/matterbridge/discordgo"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
package transmitter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/matterbridge/discordgo"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,11 @@ package bdiscord
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/matterbridge/discordgo"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks
 | 
			
		||||
@@ -42,12 +43,66 @@ func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) webhookSendTextOnly(msg *config.Message, channelID string) (string, error) {
 | 
			
		||||
	msgParts := helper.ClipOrSplitMessage(msg.Text, MessageLength, b.GetString("MessageClipped"), b.GetInt("MessageSplitMaxCount"))
 | 
			
		||||
	msgIds := []string{}
 | 
			
		||||
	b.Log.Debugf("Final avatar URL: %s", msg.Avatar)
 | 
			
		||||
	for _, msgPart := range msgParts {
 | 
			
		||||
		res, err := b.transmitter.Send(
 | 
			
		||||
			channelID,
 | 
			
		||||
			&discordgo.WebhookParams{
 | 
			
		||||
				Content:         msgPart,
 | 
			
		||||
				Username:        msg.Username,
 | 
			
		||||
				AvatarURL:       msg.Avatar,
 | 
			
		||||
				AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		} else {
 | 
			
		||||
			msgIds = append(msgIds, res.ID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// Exploit that a discord message ID is actually just a large number, so we encode a list of IDs by separating them with ";".
 | 
			
		||||
	return strings.Join(msgIds, ";"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) webhookSendFilesOnly(msg *config.Message, channelID string) error {
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo) //nolint:forcetypeassert
 | 
			
		||||
		file := discordgo.File{
 | 
			
		||||
			Name:        fi.Name,
 | 
			
		||||
			ContentType: "",
 | 
			
		||||
			Reader:      bytes.NewReader(*fi.Data),
 | 
			
		||||
		}
 | 
			
		||||
		content := fi.Comment
 | 
			
		||||
 | 
			
		||||
		// Cannot use the resulting ID for any edits anyway, so throw it away.
 | 
			
		||||
		// This has to be re-enabled when we implement message deletion.
 | 
			
		||||
		_, err := b.transmitter.Send(
 | 
			
		||||
			channelID,
 | 
			
		||||
			&discordgo.WebhookParams{
 | 
			
		||||
				Username:        msg.Username,
 | 
			
		||||
				AvatarURL:       msg.Avatar,
 | 
			
		||||
				Files:           []*discordgo.File{&file},
 | 
			
		||||
				Content:         content,
 | 
			
		||||
				AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// webhookSend send one or more message via webhook, taking care of file
 | 
			
		||||
// uploads (from slack, telegram or mattermost).
 | 
			
		||||
// Returns messageID and error.
 | 
			
		||||
func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) {
 | 
			
		||||
func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (string, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		res *discordgo.Message
 | 
			
		||||
		res string
 | 
			
		||||
		err error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
@@ -60,47 +115,13 @@ func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordg
 | 
			
		||||
 | 
			
		||||
	// We can't send empty messages.
 | 
			
		||||
	if msg.Text != "" {
 | 
			
		||||
		res, err = b.transmitter.Send(
 | 
			
		||||
			channelID,
 | 
			
		||||
			&discordgo.WebhookParams{
 | 
			
		||||
				Content:         msg.Text,
 | 
			
		||||
				Username:        msg.Username,
 | 
			
		||||
				AvatarURL:       msg.Avatar,
 | 
			
		||||
				AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
 | 
			
		||||
		}
 | 
			
		||||
		res, err = b.webhookSendTextOnly(msg, channelID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		for _, f := range msg.Extra["file"] {
 | 
			
		||||
			fi := f.(config.FileInfo)
 | 
			
		||||
			file := discordgo.File{
 | 
			
		||||
				Name:        fi.Name,
 | 
			
		||||
				ContentType: "",
 | 
			
		||||
				Reader:      bytes.NewReader(*fi.Data),
 | 
			
		||||
			}
 | 
			
		||||
			content := ""
 | 
			
		||||
			if msg.Text == "" {
 | 
			
		||||
				content = fi.Comment
 | 
			
		||||
			}
 | 
			
		||||
			_, e2 := b.transmitter.Send(
 | 
			
		||||
				channelID,
 | 
			
		||||
				&discordgo.WebhookParams{
 | 
			
		||||
					Username:        msg.Username,
 | 
			
		||||
					AvatarURL:       msg.Avatar,
 | 
			
		||||
					File:            &file,
 | 
			
		||||
					Content:         content,
 | 
			
		||||
					AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
				},
 | 
			
		||||
			)
 | 
			
		||||
			if e2 != nil {
 | 
			
		||||
				b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	if err == nil && msg.Extra != nil {
 | 
			
		||||
		err = b.webhookSendFilesOnly(msg, channelID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -116,35 +137,44 @@ func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (st
 | 
			
		||||
		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 != "" {
 | 
			
		||||
		// Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";".
 | 
			
		||||
		msgIds := strings.Split(msg.ID, ";")
 | 
			
		||||
		msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds))
 | 
			
		||||
		for len(msgParts) < len(msgIds) {
 | 
			
		||||
			msgParts = append(msgParts, "((obsoleted by edit))")
 | 
			
		||||
		}
 | 
			
		||||
		b.Log.Debugf("Editing webhook message")
 | 
			
		||||
		err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{
 | 
			
		||||
			Content:         msg.Text,
 | 
			
		||||
			Username:        msg.Username,
 | 
			
		||||
			AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
		})
 | 
			
		||||
		if err == nil {
 | 
			
		||||
		var editErr error = nil
 | 
			
		||||
		for i := range msgParts {
 | 
			
		||||
			// In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates.
 | 
			
		||||
			// TODO: Optimize away noop-updates of un-edited messages
 | 
			
		||||
			editErr = b.transmitter.Edit(channelID, msgIds[i], &discordgo.WebhookParams{
 | 
			
		||||
				Content:         msgParts[i],
 | 
			
		||||
				Username:        msg.Username,
 | 
			
		||||
				AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
			})
 | 
			
		||||
			if editErr != nil {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if editErr == nil {
 | 
			
		||||
			return msg.ID, nil
 | 
			
		||||
		}
 | 
			
		||||
		b.Log.Errorf("Could not edit webhook message: %s", err)
 | 
			
		||||
		b.Log.Errorf("Could not edit webhook message(s): %s; sending as new message(s) instead", editErr)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Processing webhook sending for message %#v", msg)
 | 
			
		||||
	discordMsg, err := b.webhookSend(msg, channelID)
 | 
			
		||||
	msg.Text = b.replaceUserMentions(msg.Text)
 | 
			
		||||
	msgID, err := b.webhookSend(msg, channelID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msg, err)
 | 
			
		||||
		b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msgID, err)
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	if discordMsg == nil {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return discordMsg.ID, nil
 | 
			
		||||
	return msgID, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,182 +0,0 @@
 | 
			
		||||
package bgitter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/go-gitter"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Bgitter struct {
 | 
			
		||||
	c     *gitter.Gitter
 | 
			
		||||
	User  *gitter.User
 | 
			
		||||
	Users []gitter.User
 | 
			
		||||
	Rooms []gitter.Room
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	return &Bgitter{Config: cfg}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bgitter) Connect() error {
 | 
			
		||||
	var err error
 | 
			
		||||
	b.Log.Info("Connecting")
 | 
			
		||||
	b.c = gitter.New(b.GetString("Token"))
 | 
			
		||||
	b.User, err = b.c.GetUser()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.Rooms, err = b.c.GetRooms()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bgitter) Disconnect() error {
 | 
			
		||||
	return nil
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	roomID, err := b.c.GetRoomId(channel.Name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Could not find roomID for %v. Please create the room on gitter.im", channel.Name)
 | 
			
		||||
	}
 | 
			
		||||
	room, err := b.c.GetRoom(roomID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.Rooms = append(b.Rooms, *room)
 | 
			
		||||
	user, err := b.c.GetUser()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	_, err = b.c.JoinRoom(roomID, user.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	users, _ := b.c.GetUsersInRoom(roomID)
 | 
			
		||||
	b.Users = append(b.Users, users...)
 | 
			
		||||
	stream := b.c.Stream(roomID)
 | 
			
		||||
	go b.c.Listen(stream)
 | 
			
		||||
 | 
			
		||||
	go func(stream *gitter.Stream, room string) {
 | 
			
		||||
		for event := range stream.Event {
 | 
			
		||||
			switch ev := event.Data.(type) {
 | 
			
		||||
			case *gitter.MessageReceived:
 | 
			
		||||
				// ignore message sent from ourselves
 | 
			
		||||
				if ev.Message.From.ID != b.User.ID {
 | 
			
		||||
					b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
 | 
			
		||||
					rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
 | 
			
		||||
						Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID,
 | 
			
		||||
						ID: ev.Message.ID}
 | 
			
		||||
					if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) {
 | 
			
		||||
						rmsg.Event = config.EventUserAction
 | 
			
		||||
						rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1)
 | 
			
		||||
					}
 | 
			
		||||
					b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
					b.Remote <- rmsg
 | 
			
		||||
				}
 | 
			
		||||
			case *gitter.GitterConnectionClosed:
 | 
			
		||||
				b.Log.Errorf("connection with gitter closed for room %s", room)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}(stream, room.URI)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bgitter) Send(msg config.Message) (string, error) {
 | 
			
		||||
	b.Log.Debugf("=> Receiving %#v", msg)
 | 
			
		||||
	roomID := b.getRoomID(msg.Channel)
 | 
			
		||||
	if roomID == "" {
 | 
			
		||||
		b.Log.Errorf("Could not find roomID for %v", msg.Channel)
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Delete message
 | 
			
		||||
	if msg.Event == config.EventMsgDelete {
 | 
			
		||||
		if msg.ID == "" {
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
		// gitter has no delete message api so we edit message to ""
 | 
			
		||||
		_, err := b.c.UpdateMessage(roomID, msg.ID, "")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Upload a file (in gitter case send the upload URL because gitter has no native upload support)
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
			
		||||
			b.c.SendMessage(roomID, rmsg.Username+rmsg.Text)
 | 
			
		||||
		}
 | 
			
		||||
		if len(msg.Extra["file"]) > 0 {
 | 
			
		||||
			return b.handleUploadFile(&msg, roomID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Edit message
 | 
			
		||||
	if msg.ID != "" {
 | 
			
		||||
		b.Log.Debugf("updating message with id %s", msg.ID)
 | 
			
		||||
		_, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post normal message
 | 
			
		||||
	resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return resp.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bgitter) getRoomID(channel string) string {
 | 
			
		||||
	for _, v := range b.Rooms {
 | 
			
		||||
		if v.URI == channel {
 | 
			
		||||
			return v.ID
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bgitter) getAvatar(user string) string {
 | 
			
		||||
	var avatar string
 | 
			
		||||
	if b.Users != nil {
 | 
			
		||||
		for _, u := range b.Users {
 | 
			
		||||
			if user == u.Username {
 | 
			
		||||
				return u.AvatarURLSmall
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return avatar
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bgitter) handleUploadFile(msg *config.Message, roomID string) (string, error) {
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
		if fi.Comment != "" {
 | 
			
		||||
			msg.Text += fi.Comment + ": "
 | 
			
		||||
		}
 | 
			
		||||
		if fi.URL != "" {
 | 
			
		||||
			msg.Text = fi.URL
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				msg.Text = fi.Comment + ": " + fi.URL
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		_, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										252
									
								
								bridge/harmony/harmony.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
 | 
			
		||||
}
 | 
			
		||||
@@ -86,6 +86,12 @@ func GetSubLines(message string, maxLineLength int, clippingMessage string) []st
 | 
			
		||||
 | 
			
		||||
	var lines []string
 | 
			
		||||
	for _, line := range strings.Split(strings.TrimSpace(message), "\n") {
 | 
			
		||||
		if line == "" {
 | 
			
		||||
			// Prevent sending empty messages, so we'll skip this line
 | 
			
		||||
			// if it has no content.
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if maxLineLength == 0 || len([]byte(line)) <= maxLineLength {
 | 
			
		||||
			lines = append(lines, line)
 | 
			
		||||
			continue
 | 
			
		||||
@@ -168,17 +174,23 @@ func HandleDownloadSize(logger *logrus.Entry, msg *config.Message, name string,
 | 
			
		||||
 | 
			
		||||
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
 | 
			
		||||
func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
 | 
			
		||||
	HandleDownloadData2(logger, msg, name, "", comment, url, data, general)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
 | 
			
		||||
func HandleDownloadData2(logger *logrus.Entry, msg *config.Message, name, id, comment, url string, data *[]byte, general *config.Protocol) {
 | 
			
		||||
	var avatar bool
 | 
			
		||||
	logger.Debugf("Download OK %#v %#v", name, len(*data))
 | 
			
		||||
	if msg.Event == config.EventAvatarDownload {
 | 
			
		||||
		avatar = true
 | 
			
		||||
	}
 | 
			
		||||
	msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{
 | 
			
		||||
		Name:    name,
 | 
			
		||||
		Data:    data,
 | 
			
		||||
		URL:     url,
 | 
			
		||||
		Comment: comment,
 | 
			
		||||
		Avatar:  avatar,
 | 
			
		||||
		Name:     name,
 | 
			
		||||
		Data:     data,
 | 
			
		||||
		URL:      url,
 | 
			
		||||
		Comment:  comment,
 | 
			
		||||
		Avatar:   avatar,
 | 
			
		||||
		NativeID: id,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -199,14 +211,51 @@ func ClipMessage(text string, length int, clippingMessage string) string {
 | 
			
		||||
 | 
			
		||||
	if len(text) > length {
 | 
			
		||||
		text = text[:length-len(clippingMessage)]
 | 
			
		||||
		if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
 | 
			
		||||
			text = text[:len(text)-size]
 | 
			
		||||
		for len(text) > 0 {
 | 
			
		||||
			if r, _ := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
 | 
			
		||||
				text = text[:len(text)-1]
 | 
			
		||||
				// Note: DecodeLastRuneInString only returns the constant value "1" in
 | 
			
		||||
				// case of an error. We do not yet know whether the last rune is now
 | 
			
		||||
				// actually valid. Example: "€" is 0xE2 0x82 0xAC. If we happen to split
 | 
			
		||||
				// the string just before 0xAC, and go back only one byte, that would
 | 
			
		||||
				// leave us with a string that ends in the byte 0xE2, which is not a valid
 | 
			
		||||
				// rune, so we need to try again.
 | 
			
		||||
			} else {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		text += clippingMessage
 | 
			
		||||
	}
 | 
			
		||||
	return text
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ClipOrSplitMessage(text string, length int, clippingMessage string, splitMax int) []string {
 | 
			
		||||
	var msgParts []string
 | 
			
		||||
	remainingText := text
 | 
			
		||||
	// Invariant of this splitting loop: No text is lost (msgParts+remainingText is the original text),
 | 
			
		||||
	// and all parts is guaranteed to satisfy the length requirement.
 | 
			
		||||
	for len(msgParts) < splitMax-1 && len(remainingText) > length {
 | 
			
		||||
		// Decision: The text needs to be split (again).
 | 
			
		||||
		var chunk string
 | 
			
		||||
		wasted := 0
 | 
			
		||||
		// The longest UTF-8 encoding of a valid rune is 4 bytes (0xF4 0x8F 0xBF 0xBF, encoding U+10FFFF),
 | 
			
		||||
		// so we should never need to waste 4 or more bytes at a time.
 | 
			
		||||
		for wasted < 4 && wasted < length {
 | 
			
		||||
			chunk = remainingText[:length-wasted]
 | 
			
		||||
			if r, _ := utf8.DecodeLastRuneInString(chunk); r == utf8.RuneError {
 | 
			
		||||
				wasted += 1
 | 
			
		||||
			} else {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// Note: At this point, "chunk" might still be invalid, if "text" is very broken.
 | 
			
		||||
		msgParts = append(msgParts, chunk)
 | 
			
		||||
		remainingText = remainingText[len(chunk):]
 | 
			
		||||
	}
 | 
			
		||||
	msgParts = append(msgParts, ClipMessage(remainingText, length, clippingMessage))
 | 
			
		||||
	return msgParts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseMarkdown takes in an input string as markdown and parses it to html
 | 
			
		||||
func ParseMarkdown(input string) string {
 | 
			
		||||
	extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,15 @@ var lineSplittingTestCases = map[string]struct {
 | 
			
		||||
		},
 | 
			
		||||
		nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
 | 
			
		||||
	},
 | 
			
		||||
	"Long message, clip three-byte rune after two bytes": {
 | 
			
		||||
		input: "x 人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。",
 | 
			
		||||
		splitOutput: []string{
 | 
			
		||||
			"x 人人生而自由,在尊嚴和權利上 <clipped message>",
 | 
			
		||||
			"一律平等。 他們都具有理性和良知 <clipped message>",
 | 
			
		||||
			",應該以兄弟情誼的精神對待彼此。",
 | 
			
		||||
		},
 | 
			
		||||
		nonSplitOutput: []string{"x 人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。"},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetSubLines(t *testing.T) {
 | 
			
		||||
@@ -125,3 +134,105 @@ func TestConvertWebPToPNG(t *testing.T) {
 | 
			
		||||
		t.Fail()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var clippingOrSplittingTestCases = map[string]struct {
 | 
			
		||||
	inputText       string
 | 
			
		||||
	clipSplitLength int
 | 
			
		||||
	clippingMessage string
 | 
			
		||||
	splitMax        int
 | 
			
		||||
	expectedOutput  []string
 | 
			
		||||
}{
 | 
			
		||||
	"Short single-line message, split 3": {
 | 
			
		||||
		inputText:       "short",
 | 
			
		||||
		clipSplitLength: 20,
 | 
			
		||||
		clippingMessage: "?!?!",
 | 
			
		||||
		splitMax:        3,
 | 
			
		||||
		expectedOutput:  []string{"short"},
 | 
			
		||||
	},
 | 
			
		||||
	"Short single-line message, split 1": {
 | 
			
		||||
		inputText:       "short",
 | 
			
		||||
		clipSplitLength: 20,
 | 
			
		||||
		clippingMessage: "?!?!",
 | 
			
		||||
		splitMax:        1,
 | 
			
		||||
		expectedOutput:  []string{"short"},
 | 
			
		||||
	},
 | 
			
		||||
	"Short single-line message, split 0": {
 | 
			
		||||
		// Mainly check that we don't crash.
 | 
			
		||||
		inputText:       "short",
 | 
			
		||||
		clipSplitLength: 20,
 | 
			
		||||
		clippingMessage: "?!?!",
 | 
			
		||||
		splitMax:        0,
 | 
			
		||||
		expectedOutput:  []string{"short"},
 | 
			
		||||
	},
 | 
			
		||||
	"Long single-line message, noclip": {
 | 
			
		||||
		inputText:       "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
 | 
			
		||||
		clipSplitLength: 50,
 | 
			
		||||
		clippingMessage: "?!?!",
 | 
			
		||||
		splitMax:        10,
 | 
			
		||||
		expectedOutput: []string{
 | 
			
		||||
			"Lorem ipsum dolor sit amet, consectetur adipiscing",
 | 
			
		||||
			" elit, sed do eiusmod tempor incididunt ut labore ",
 | 
			
		||||
			"et dolore magna aliqua.",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	"Long single-line message, noclip tight": {
 | 
			
		||||
		inputText:       "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
 | 
			
		||||
		clipSplitLength: 50,
 | 
			
		||||
		clippingMessage: "?!?!",
 | 
			
		||||
		splitMax:        3,
 | 
			
		||||
		expectedOutput: []string{
 | 
			
		||||
			"Lorem ipsum dolor sit amet, consectetur adipiscing",
 | 
			
		||||
			" elit, sed do eiusmod tempor incididunt ut labore ",
 | 
			
		||||
			"et dolore magna aliqua.",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	"Long single-line message, clip custom": {
 | 
			
		||||
		inputText:       "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
 | 
			
		||||
		clipSplitLength: 50,
 | 
			
		||||
		clippingMessage: "?!?!",
 | 
			
		||||
		splitMax:        2,
 | 
			
		||||
		expectedOutput: []string{
 | 
			
		||||
			"Lorem ipsum dolor sit amet, consectetur adipiscing",
 | 
			
		||||
			" elit, sed do eiusmod tempor incididunt ut lab?!?!",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	"Long single-line message, clip built-in": {
 | 
			
		||||
		inputText:       "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
 | 
			
		||||
		clipSplitLength: 50,
 | 
			
		||||
		clippingMessage: "",
 | 
			
		||||
		splitMax:        2,
 | 
			
		||||
		expectedOutput: []string{
 | 
			
		||||
			"Lorem ipsum dolor sit amet, consectetur adipiscing",
 | 
			
		||||
			" elit, sed do eiusmod tempor inc <clipped message>",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	"Short multi-line message": {
 | 
			
		||||
		inputText:       "I\ncan't\nget\nno\nsatisfaction!",
 | 
			
		||||
		clipSplitLength: 50,
 | 
			
		||||
		clippingMessage: "",
 | 
			
		||||
		splitMax:        2,
 | 
			
		||||
		expectedOutput:  []string{"I\ncan't\nget\nno\nsatisfaction!"},
 | 
			
		||||
	},
 | 
			
		||||
	"Long message containing UTF-8 multi-byte runes": {
 | 
			
		||||
		inputText:       "人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。",
 | 
			
		||||
		clipSplitLength: 50,
 | 
			
		||||
		clippingMessage: "",
 | 
			
		||||
		splitMax:        10,
 | 
			
		||||
		expectedOutput: []string{
 | 
			
		||||
			"人人生而自由,在尊嚴和權利上一律",  // Note: only 48 bytes!
 | 
			
		||||
			"平等。 他們都具有理性和良知,應該", // Note: only 49 bytes!
 | 
			
		||||
			"以兄弟情誼的精神對待彼此。",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestClipOrSplitMessage(t *testing.T) {
 | 
			
		||||
	for testname, testcase := range clippingOrSplittingTestCases {
 | 
			
		||||
		actualOutput := ClipOrSplitMessage(testcase.inputText, testcase.clipSplitLength, testcase.clippingMessage, testcase.splitMax)
 | 
			
		||||
		assert.Equalf(t, testcase.expectedOutput, actualOutput, "'%s' testcase should give expected lines with clipping+splitting.", testname)
 | 
			
		||||
		for _, splitLine := range testcase.expectedOutput {
 | 
			
		||||
			byteLength := len([]byte(splitLine))
 | 
			
		||||
			assert.True(t, byteLength <= testcase.clipSplitLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testname, testcase.clipSplitLength, byteLength)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
//go:build cgo
 | 
			
		||||
// +build cgo
 | 
			
		||||
//go:build cgolottie
 | 
			
		||||
 | 
			
		||||
package helper
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
// +build !cgo
 | 
			
		||||
//go:build !cgolottie
 | 
			
		||||
 | 
			
		||||
package helper
 | 
			
		||||
 | 
			
		||||
@@ -6,6 +6,7 @@ import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								bridge/irc/charset.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								bridge/irc/charset.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
package birc
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"golang.org/x/text/encoding"
 | 
			
		||||
	"golang.org/x/text/encoding/japanese"
 | 
			
		||||
	"golang.org/x/text/encoding/korean"
 | 
			
		||||
	"golang.org/x/text/encoding/simplifiedchinese"
 | 
			
		||||
	"golang.org/x/text/encoding/traditionalchinese"
 | 
			
		||||
	"golang.org/x/text/encoding/unicode"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var encoders = map[string]encoding.Encoding{
 | 
			
		||||
	"utf-8":       unicode.UTF8,
 | 
			
		||||
	"iso-2022-jp": japanese.ISO2022JP,
 | 
			
		||||
	"big5":        traditionalchinese.Big5,
 | 
			
		||||
	"gbk":         simplifiedchinese.GBK,
 | 
			
		||||
	"euc-kr":      korean.EUCKR,
 | 
			
		||||
	"gb2312":      simplifiedchinese.HZGB2312,
 | 
			
		||||
	"shift-jis":   japanese.ShiftJIS,
 | 
			
		||||
	"euc-jp":      japanese.EUCJP,
 | 
			
		||||
	"gb18030":     simplifiedchinese.GB18030,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func toUTF8(from string, input string) string {
 | 
			
		||||
	enc, ok := encoders[from]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return input
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res, _ := enc.NewDecoder().String(input)
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
@@ -11,7 +11,6 @@ import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/lrstanley/girc"
 | 
			
		||||
	"github.com/missdeer/golib/ic"
 | 
			
		||||
	"github.com/paulrosania/go-charset/charset"
 | 
			
		||||
	"github.com/saintfish/chardet"
 | 
			
		||||
 | 
			
		||||
@@ -24,12 +23,12 @@ func (b *Birc) handleCharset(msg *config.Message) error {
 | 
			
		||||
	if b.GetString("Charset") != "" {
 | 
			
		||||
		switch b.GetString("Charset") {
 | 
			
		||||
		case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
 | 
			
		||||
			msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text)
 | 
			
		||||
			msg.Text = toUTF8(b.GetString("Charset"), msg.Text)
 | 
			
		||||
		default:
 | 
			
		||||
			buf := new(bytes.Buffer)
 | 
			
		||||
			w, err := charset.NewWriter(b.GetString("Charset"), buf)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Errorf("charset from utf-8 conversion failed: %s", err)
 | 
			
		||||
				b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Fprint(w, msg.Text)
 | 
			
		||||
@@ -123,8 +122,18 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
 | 
			
		||||
	i := b.i
 | 
			
		||||
	b.Nick = event.Params[0]
 | 
			
		||||
 | 
			
		||||
	b.Log.Debug("Clearing handlers before adding in case of BNC reconnect")
 | 
			
		||||
	i.Handlers.Clear("PRIVMSG")
 | 
			
		||||
	i.Handlers.Clear("CTCP_ACTION")
 | 
			
		||||
	i.Handlers.Clear(girc.RPL_TOPICWHOTIME)
 | 
			
		||||
	i.Handlers.Clear(girc.NOTICE)
 | 
			
		||||
	i.Handlers.Clear("JOIN")
 | 
			
		||||
	i.Handlers.Clear("PART")
 | 
			
		||||
	i.Handlers.Clear("QUIT")
 | 
			
		||||
	i.Handlers.Clear("KICK")
 | 
			
		||||
	i.Handlers.Clear("INVITE")
 | 
			
		||||
 | 
			
		||||
	i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg)
 | 
			
		||||
	i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg)
 | 
			
		||||
	i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
 | 
			
		||||
	i.Handlers.AddBg(girc.NOTICE, b.handleNotice)
 | 
			
		||||
	i.Handlers.AddBg("JOIN", b.handleJoinPart)
 | 
			
		||||
@@ -196,7 +205,11 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
 | 
			
		||||
	b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
 | 
			
		||||
 | 
			
		||||
	// set action event
 | 
			
		||||
	if event.IsAction() {
 | 
			
		||||
	if ok, ctcp := event.IsCTCP(); ok {
 | 
			
		||||
		if ctcp.Command != girc.CTCP_ACTION {
 | 
			
		||||
			b.Log.Debugf("dropping user ctcp, command: %s", ctcp.Command)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		rmsg.Event = config.EventUserAction
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -227,7 +240,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
 | 
			
		||||
	}
 | 
			
		||||
	switch mycharset {
 | 
			
		||||
	case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
 | 
			
		||||
		rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text)
 | 
			
		||||
		rmsg.Text = toUTF8(b.GetString("Charset"), rmsg.Text)
 | 
			
		||||
	default:
 | 
			
		||||
		r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -244,6 +257,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
 | 
			
		||||
 | 
			
		||||
func (b *Birc) handleRunCommands() {
 | 
			
		||||
	for _, cmd := range b.GetStringSlice("RunCommands") {
 | 
			
		||||
		cmd = strings.ReplaceAll(cmd, "{BOTNICK}", b.Nick)
 | 
			
		||||
		if err := b.i.Cmd.SendRaw(cmd); err != nil {
 | 
			
		||||
			b.Log.Errorf("RunCommands %s failed: %s", cmd, err)
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package birc
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"hash/crc32"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
@@ -72,6 +73,10 @@ func (b *Birc) Command(msg *config.Message) string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Birc) Connect() error {
 | 
			
		||||
	if b.GetBool("UseSASL") && b.GetString("TLSClientCertificate") != "" {
 | 
			
		||||
		return errors.New("you can't enable SASL and TLSClientCertificate at the same time")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Local = make(chan config.Message, b.MessageQueue+10)
 | 
			
		||||
	b.Log.Infof("Connecting %s", b.GetString("Server"))
 | 
			
		||||
 | 
			
		||||
@@ -300,6 +305,11 @@ func (b *Birc) getClient() (*girc.Client, error) {
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("setting pingdelay to %s", pingDelay)
 | 
			
		||||
 | 
			
		||||
	tlsConfig, err := b.getTLSConfig()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	i := girc.New(girc.Config{
 | 
			
		||||
		Server:     server,
 | 
			
		||||
		ServerPass: b.GetString("Password"),
 | 
			
		||||
@@ -308,7 +318,8 @@ func (b *Birc) getClient() (*girc.Client, error) {
 | 
			
		||||
		User:       user,
 | 
			
		||||
		Name:       realName,
 | 
			
		||||
		SSL:        b.GetBool("UseTLS"),
 | 
			
		||||
		TLSConfig:  &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec
 | 
			
		||||
		Bind:       b.GetString("Bind"),
 | 
			
		||||
		TLSConfig:  tlsConfig,
 | 
			
		||||
		PingDelay:  pingDelay,
 | 
			
		||||
		// skip gIRC internal rate limiting, since we have our own throttling
 | 
			
		||||
		AllowFlood:    true,
 | 
			
		||||
@@ -351,8 +362,10 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	// don't forward message from ourself
 | 
			
		||||
	if event.Source.Name == b.Nick {
 | 
			
		||||
		return true
 | 
			
		||||
	if event.Source != nil {
 | 
			
		||||
		if event.Source.Name == b.Nick {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// don't forward messages we sent via RELAYMSG
 | 
			
		||||
	if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick {
 | 
			
		||||
@@ -380,3 +393,23 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
 | 
			
		||||
func (b *Birc) formatnicks(nicks []string) string {
 | 
			
		||||
	return strings.Join(nicks, ", ") + " currently on IRC"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Birc) getTLSConfig() (*tls.Config, error) {
 | 
			
		||||
	server, _, _ := net.SplitHostPort(b.GetString("server"))
 | 
			
		||||
 | 
			
		||||
	tlsConfig := &tls.Config{
 | 
			
		||||
		InsecureSkipVerify: b.GetBool("skiptlsverify"), //nolint:gosec
 | 
			
		||||
		ServerName:         server,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if filename := b.GetString("TLSClientCertificate"); filename != "" {
 | 
			
		||||
		cert, err := tls.LoadX509KeyPair(filename, filename)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tlsConfig.Certificates = []tls.Certificate{cert}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return tlsConfig, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	matrix "github.com/matrix-org/gomatrix"
 | 
			
		||||
	matrix "github.com/matterbridge/gomatrix"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func newMatrixUsername(username string) *matrixUsername {
 | 
			
		||||
@@ -51,7 +51,7 @@ func interface2Struct(in interface{}, out interface{}) error {
 | 
			
		||||
	return json.Unmarshal(jsonObj, out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getDisplayName retrieves the displayName for mxid, querying the homserver if the mxid is not in the cache.
 | 
			
		||||
// getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache.
 | 
			
		||||
func (b *Bmatrix) getDisplayName(mxid string) string {
 | 
			
		||||
	if b.GetBool("UseUserName") {
 | 
			
		||||
		return mxid[1:]
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	matrix "github.com/matrix-org/gomatrix"
 | 
			
		||||
	matrix "github.com/matterbridge/gomatrix"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -48,8 +48,10 @@ type matrixUsername struct {
 | 
			
		||||
 | 
			
		||||
// SubTextMessage represents the new content of the message in edit messages.
 | 
			
		||||
type SubTextMessage struct {
 | 
			
		||||
	MsgType string `json:"msgtype"`
 | 
			
		||||
	Body    string `json:"body"`
 | 
			
		||||
	MsgType       string `json:"msgtype"`
 | 
			
		||||
	Body          string `json:"body"`
 | 
			
		||||
	FormattedBody string `json:"formatted_body,omitempty"`
 | 
			
		||||
	Format        string `json:"format,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MessageRelation explains how the current message relates to a previous message.
 | 
			
		||||
@@ -65,6 +67,19 @@ type EditedMessage struct {
 | 
			
		||||
	matrix.TextMessage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type InReplyToRelationContent struct {
 | 
			
		||||
	EventID string `json:"event_id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type InReplyToRelation struct {
 | 
			
		||||
	InReplyTo InReplyToRelationContent `json:"m.in_reply_to"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ReplyMessage struct {
 | 
			
		||||
	RelatedTo InReplyToRelation `json:"m.relates_to"`
 | 
			
		||||
	matrix.TextMessage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	b := &Bmatrix{Config: cfg}
 | 
			
		||||
	b.RoomMap = make(map[string]string)
 | 
			
		||||
@@ -133,12 +148,43 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
 | 
			
		||||
 | 
			
		||||
	username := newMatrixUsername(msg.Username)
 | 
			
		||||
 | 
			
		||||
	body := username.plain + msg.Text
 | 
			
		||||
	formattedBody := username.formatted + helper.ParseMarkdown(msg.Text)
 | 
			
		||||
 | 
			
		||||
	if b.GetBool("SpoofUsername") {
 | 
			
		||||
		// https://spec.matrix.org/v1.3/client-server-api/#mroommember
 | 
			
		||||
		type stateMember struct {
 | 
			
		||||
			AvatarURL   string `json:"avatar_url,omitempty"`
 | 
			
		||||
			DisplayName string `json:"displayname"`
 | 
			
		||||
			Membership  string `json:"membership"`
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// TODO: reset username afterwards with DisplayName: null ?
 | 
			
		||||
		m := stateMember{
 | 
			
		||||
			AvatarURL:   "",
 | 
			
		||||
			DisplayName: username.plain,
 | 
			
		||||
			Membership:  "join",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err := b.mc.SendStateEvent(channel, "m.room.member", b.UserID, m)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			body = msg.Text
 | 
			
		||||
			formattedBody = helper.ParseMarkdown(msg.Text)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Make a action /me of the message
 | 
			
		||||
	if msg.Event == config.EventUserAction {
 | 
			
		||||
		m := matrix.TextMessage{
 | 
			
		||||
			MsgType:       "m.emote",
 | 
			
		||||
			Body:          username.plain + msg.Text,
 | 
			
		||||
			FormattedBody: username.formatted + msg.Text,
 | 
			
		||||
			Body:          body,
 | 
			
		||||
			FormattedBody: formattedBody,
 | 
			
		||||
			Format:        "org.matrix.custom.html",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.GetBool("HTMLDisable") {
 | 
			
		||||
			m.Format = ""
 | 
			
		||||
			m.FormattedBody = ""
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		msgID := ""
 | 
			
		||||
@@ -201,20 +247,29 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
 | 
			
		||||
 | 
			
		||||
	// Edit message if we have an ID
 | 
			
		||||
	if msg.ID != "" {
 | 
			
		||||
		rmsg := EditedMessage{TextMessage: matrix.TextMessage{
 | 
			
		||||
			Body:    username.plain + msg.Text,
 | 
			
		||||
			MsgType: "m.text",
 | 
			
		||||
		}}
 | 
			
		||||
		if b.GetBool("HTMLDisable") {
 | 
			
		||||
			rmsg.TextMessage.FormattedBody = username.formatted + "* " + msg.Text
 | 
			
		||||
		} else {
 | 
			
		||||
			rmsg.Format = "org.matrix.custom.html"
 | 
			
		||||
			rmsg.TextMessage.FormattedBody = username.formatted + "* " + helper.ParseMarkdown(msg.Text)
 | 
			
		||||
		rmsg := EditedMessage{
 | 
			
		||||
			TextMessage: matrix.TextMessage{
 | 
			
		||||
				Body:          body,
 | 
			
		||||
				MsgType:       "m.text",
 | 
			
		||||
				Format:        "org.matrix.custom.html",
 | 
			
		||||
				FormattedBody: formattedBody,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		rmsg.NewContent = SubTextMessage{
 | 
			
		||||
			Body:    rmsg.TextMessage.Body,
 | 
			
		||||
			MsgType: "m.text",
 | 
			
		||||
			Body:          rmsg.TextMessage.Body,
 | 
			
		||||
			FormattedBody: rmsg.TextMessage.FormattedBody,
 | 
			
		||||
			Format:        rmsg.TextMessage.Format,
 | 
			
		||||
			MsgType:       "m.text",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.GetBool("HTMLDisable") {
 | 
			
		||||
			rmsg.TextMessage.Format = ""
 | 
			
		||||
			rmsg.TextMessage.FormattedBody = ""
 | 
			
		||||
			rmsg.NewContent.Format = ""
 | 
			
		||||
			rmsg.NewContent.FormattedBody = ""
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		rmsg.RelatedTo = MessageRelation{
 | 
			
		||||
			EventID: msg.ID,
 | 
			
		||||
			Type:    "m.replace",
 | 
			
		||||
@@ -236,8 +291,52 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
 | 
			
		||||
	if msg.Event == config.EventJoinLeave {
 | 
			
		||||
		m := matrix.TextMessage{
 | 
			
		||||
			MsgType:       "m.notice",
 | 
			
		||||
			Body:          username.plain + msg.Text,
 | 
			
		||||
			FormattedBody: username.formatted + msg.Text,
 | 
			
		||||
			Body:          body,
 | 
			
		||||
			FormattedBody: formattedBody,
 | 
			
		||||
			Format:        "org.matrix.custom.html",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.GetBool("HTMLDisable") {
 | 
			
		||||
			m.Format = ""
 | 
			
		||||
			m.FormattedBody = ""
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var (
 | 
			
		||||
			resp *matrix.RespSendEvent
 | 
			
		||||
			err  error
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		err = b.retry(func() error {
 | 
			
		||||
			resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return resp.EventID, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.ParentValid() {
 | 
			
		||||
		m := ReplyMessage{
 | 
			
		||||
			TextMessage: matrix.TextMessage{
 | 
			
		||||
				MsgType:       "m.text",
 | 
			
		||||
				Body:          body,
 | 
			
		||||
				FormattedBody: formattedBody,
 | 
			
		||||
				Format:        "org.matrix.custom.html",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.GetBool("HTMLDisable") {
 | 
			
		||||
			m.TextMessage.Format = ""
 | 
			
		||||
			m.TextMessage.FormattedBody = ""
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		m.RelatedTo = InReplyToRelation{
 | 
			
		||||
			InReplyTo: InReplyToRelationContent{
 | 
			
		||||
				EventID: msg.ParentID,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var (
 | 
			
		||||
@@ -264,7 +363,7 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		err = b.retry(func() error {
 | 
			
		||||
			resp, err = b.mc.SendText(channel, username.plain+msg.Text)
 | 
			
		||||
			resp, err = b.mc.SendText(channel, body)
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
@@ -282,8 +381,7 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	err = b.retry(func() error {
 | 
			
		||||
		resp, err = b.mc.SendFormattedText(channel, username.plain+msg.Text,
 | 
			
		||||
			username.formatted+helper.ParseMarkdown(msg.Text))
 | 
			
		||||
		resp, err = b.mc.SendFormattedText(channel, body, formattedBody)
 | 
			
		||||
 | 
			
		||||
		return err
 | 
			
		||||
	})
 | 
			
		||||
@@ -341,6 +439,38 @@ func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool {
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool {
 | 
			
		||||
	relationInterface, present := ev.Content["m.relates_to"]
 | 
			
		||||
	if !present {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var relation InReplyToRelation
 | 
			
		||||
	if err := interface2Struct(relationInterface, &relation); err != nil {
 | 
			
		||||
		// probably fine
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body := rmsg.Text
 | 
			
		||||
 | 
			
		||||
	if !b.GetBool("keepquotedreply") {
 | 
			
		||||
		for strings.HasPrefix(body, "> ") {
 | 
			
		||||
			lineIdx := strings.IndexRune(body, '\n')
 | 
			
		||||
			if lineIdx == -1 {
 | 
			
		||||
				body = ""
 | 
			
		||||
			} else {
 | 
			
		||||
				body = body[(lineIdx + 1):]
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg.Text = body
 | 
			
		||||
	rmsg.ParentID = relation.InReplyTo.EventID
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) handleMemberChange(ev *matrix.Event) {
 | 
			
		||||
	// Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information
 | 
			
		||||
	if ev.Content["membership"] == "join" {
 | 
			
		||||
@@ -403,6 +533,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Is it a reply?
 | 
			
		||||
		if b.handleReply(ev, rmsg) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Do we have attachments
 | 
			
		||||
		if b.containsAttachment(ev.Content) {
 | 
			
		||||
			err := b.handleDownloadFile(&rmsg, ev.Content)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
package bmattermost
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterclient"
 | 
			
		||||
	matterclient6 "github.com/matterbridge/matterclient"
 | 
			
		||||
	"github.com/mattermost/mattermost-server/v5/model"
 | 
			
		||||
	model6 "github.com/mattermost/mattermost-server/v6/model"
 | 
			
		||||
	"github.com/matterbridge/matterclient"
 | 
			
		||||
	"github.com/mattermost/mattermost/server/public/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// handleDownloadAvatar downloads the avatar of userid from channel
 | 
			
		||||
@@ -26,20 +26,11 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
 | 
			
		||||
		var (
 | 
			
		||||
			data []byte
 | 
			
		||||
			err  error
 | 
			
		||||
			resp *model.Response
 | 
			
		||||
		)
 | 
			
		||||
		if b.mc6 != nil {
 | 
			
		||||
			data, _, err = b.mc6.Client.GetProfileImage(userid, "")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			data, resp = b.mc.Client.GetProfileImage(userid, "")
 | 
			
		||||
			if resp.Error != nil {
 | 
			
		||||
				b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		data, _, err = b.mc.Client.GetProfileImage(context.TODO(), userid, "")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
 | 
			
		||||
@@ -52,33 +43,10 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleDownloadFile handles file download
 | 
			
		||||
//nolint:wrapcheck
 | 
			
		||||
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)
 | 
			
		||||
	finfo, resp := b.mc.Client.GetFileInfo(id)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	data, resp := b.mc.Client.DownloadFile(id, true)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
 | 
			
		||||
	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)
 | 
			
		||||
	url, _, _ := b.mc.Client.GetFileLink(context.TODO(), id)
 | 
			
		||||
	finfo, _, err := b.mc.Client.GetFileInfo(context.TODO(), id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -86,7 +54,7 @@ func (b *Bmattermost) handleDownloadFile6(rmsg *config.Message, id string) error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	data, _, err := b.mc6.Client.DownloadFile(id, true)
 | 
			
		||||
	data, _, err := b.mc.Client.DownloadFile(context.TODO(), id, true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -125,24 +93,24 @@ func (b *Bmattermost) handleMatter() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//nolint:cyclop
 | 
			
		||||
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 {
 | 
			
		||||
		b.Log.Debugf("%#v", message.Raw.Data)
 | 
			
		||||
		b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType())
 | 
			
		||||
 | 
			
		||||
		if b.skipMessage(message) {
 | 
			
		||||
			b.Log.Debugf("Skipped message: %#v", message)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		channelName := b.getChannelName(message.Post.ChannelId)
 | 
			
		||||
		if channelName == "" {
 | 
			
		||||
			channelName = message.Channel
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// only download avatars if we have a place to upload them (configured mediaserver)
 | 
			
		||||
		if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
 | 
			
		||||
			b.handleDownloadAvatar(message.UserID, message.Channel)
 | 
			
		||||
			b.handleDownloadAvatar(message.UserID, channelName)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Log.Debugf("== Receiving event %#v", message)
 | 
			
		||||
@@ -150,7 +118,7 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
 | 
			
		||||
		rmsg := &config.Message{
 | 
			
		||||
			Username: message.Username,
 | 
			
		||||
			UserID:   message.UserID,
 | 
			
		||||
			Channel:  message.Channel,
 | 
			
		||||
			Channel:  channelName,
 | 
			
		||||
			Text:     message.Text,
 | 
			
		||||
			ID:       message.Post.Id,
 | 
			
		||||
			ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
 | 
			
		||||
@@ -161,11 +129,11 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
 | 
			
		||||
		b.handleProps(rmsg, message)
 | 
			
		||||
 | 
			
		||||
		// create a text for bridges that don't support native editing
 | 
			
		||||
		if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") {
 | 
			
		||||
		if message.Raw.EventType() == model.WebsocketEventPostEdited && !b.GetBool("EditDisable") {
 | 
			
		||||
			rmsg.Text = message.Text + b.GetString("EditSuffix")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED {
 | 
			
		||||
		if message.Raw.EventType() == model.WebsocketEventPostDeleted {
 | 
			
		||||
			rmsg.Event = config.EventMsgDelete
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -177,65 +145,12 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Use nickname instead of username if defined
 | 
			
		||||
		if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
 | 
			
		||||
			rmsg.Username = nick
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		messages <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// nolint:cyclop
 | 
			
		||||
func (b *Bmattermost) handleMatterClient6(messages chan *config.Message) {
 | 
			
		||||
	for message := range b.mc6.MessageChan {
 | 
			
		||||
		b.Log.Debugf("%#v", message.Raw.GetData())
 | 
			
		||||
 | 
			
		||||
		if b.skipMessage6(message) {
 | 
			
		||||
			b.Log.Debugf("Skipped message: %#v", message)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// only download avatars if we have a place to upload them (configured mediaserver)
 | 
			
		||||
		if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
 | 
			
		||||
			b.handleDownloadAvatar(message.UserID, message.Channel)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Log.Debugf("== Receiving event %#v", message)
 | 
			
		||||
 | 
			
		||||
		rmsg := &config.Message{
 | 
			
		||||
			Username: message.Username,
 | 
			
		||||
			UserID:   message.UserID,
 | 
			
		||||
			Channel:  message.Channel,
 | 
			
		||||
			Text:     message.Text,
 | 
			
		||||
			ID:       message.Post.Id,
 | 
			
		||||
			ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
 | 
			
		||||
			Extra:    make(map[string][]interface{}),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// handle mattermost post properties (override username and attachments)
 | 
			
		||||
		b.handleProps6(rmsg, message)
 | 
			
		||||
 | 
			
		||||
		// create a text for bridges that don't support native editing
 | 
			
		||||
		if message.Raw.EventType() == model6.WebsocketEventPostEdited && !b.GetBool("EditDisable") {
 | 
			
		||||
			rmsg.Text = message.Text + b.GetString("EditSuffix")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if message.Raw.EventType() == model6.WebsocketEventPostDeleted {
 | 
			
		||||
			rmsg.Event = config.EventMsgDelete
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, id := range message.Post.FileIds {
 | 
			
		||||
			err := b.handleDownloadFile(rmsg, id)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Errorf("download failed: %s", err)
 | 
			
		||||
		if !b.GetBool("useusername") {
 | 
			
		||||
			if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
 | 
			
		||||
				rmsg.Username = nick
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Use nickname instead of username if defined
 | 
			
		||||
		if nick := b.mc6.GetNickName(rmsg.UserID); nick != "" {
 | 
			
		||||
			rmsg.Username = nick
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		messages <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -244,6 +159,7 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
 | 
			
		||||
	for {
 | 
			
		||||
		message := b.mh.Receive()
 | 
			
		||||
		b.Log.Debugf("Receiving from matterhook %#v", message)
 | 
			
		||||
 | 
			
		||||
		messages <- &config.Message{
 | 
			
		||||
			UserID:   message.UserID,
 | 
			
		||||
			Username: message.UserName,
 | 
			
		||||
@@ -253,15 +169,10 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleUploadFile handles native upload of files
 | 
			
		||||
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
 | 
			
		||||
	if b.mc6 != nil {
 | 
			
		||||
		return b.handleUploadFile6(msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	var res, id string
 | 
			
		||||
	channelID := b.mc.GetChannelId(msg.Channel, b.TeamID)
 | 
			
		||||
	channelID := b.getChannelID(msg.Channel)
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
		id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
 | 
			
		||||
@@ -277,53 +188,8 @@ func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
 | 
			
		||||
	return res, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// nolint:forcetypeassert,wrapcheck
 | 
			
		||||
func (b *Bmattermost) handleUploadFile6(msg *config.Message) (string, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	var res, id string
 | 
			
		||||
	channelID := b.mc6.GetChannelID(msg.Channel, b.TeamID)
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
		id, err = b.mc6.UploadFile(*fi.Data, channelID, fi.Name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		msg.Text = fi.Comment
 | 
			
		||||
		if b.GetBool("PrefixMessagesWithNick") {
 | 
			
		||||
			msg.Text = msg.Username + msg.Text
 | 
			
		||||
		}
 | 
			
		||||
		res, err = b.mc6.PostMessageWithFiles(channelID, msg.Text, msg.ParentID, []string{id})
 | 
			
		||||
	}
 | 
			
		||||
	return res, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//nolint:forcetypeassert
 | 
			
		||||
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.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 == "" {
 | 
			
		||||
			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)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// nolint:forcetypeassert
 | 
			
		||||
func (b *Bmattermost) handleProps6(rmsg *config.Message, message *matterclient6.Message) {
 | 
			
		||||
	props := message.Post.Props
 | 
			
		||||
	if props == nil {
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -6,11 +6,9 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterclient"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterhook"
 | 
			
		||||
	matterclient6 "github.com/matterbridge/matterclient"
 | 
			
		||||
	"github.com/mattermost/mattermost-server/v5/model"
 | 
			
		||||
	model6 "github.com/mattermost/mattermost-server/v6/model"
 | 
			
		||||
	"github.com/matterbridge/matterclient"
 | 
			
		||||
	"github.com/mattermost/mattermost/server/public/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Bmattermost) doConnectWebhookBind() error {
 | 
			
		||||
@@ -24,33 +22,15 @@ func (b *Bmattermost) doConnectWebhookBind() error {
 | 
			
		||||
			})
 | 
			
		||||
	case b.GetString("Token") != "":
 | 
			
		||||
		b.Log.Info("Connecting using token (sending)")
 | 
			
		||||
		b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
			
		||||
 | 
			
		||||
		if b.v6 {
 | 
			
		||||
			err := b.apiLogin6()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			err := b.apiLogin()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		err := b.apiLogin()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	case b.GetString("Login") != "":
 | 
			
		||||
		b.Log.Info("Connecting using login/password (sending)")
 | 
			
		||||
		b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
			
		||||
 | 
			
		||||
		if b.v6 {
 | 
			
		||||
			err := b.apiLogin6()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			err := b.apiLogin()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		err := b.apiLogin()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		b.Log.Info("Connecting using webhookbindaddress (receiving)")
 | 
			
		||||
@@ -72,45 +52,28 @@ func (b *Bmattermost) doConnectWebhookURL() error {
 | 
			
		||||
		})
 | 
			
		||||
	if b.GetString("Token") != "" {
 | 
			
		||||
		b.Log.Info("Connecting using token (receiving)")
 | 
			
		||||
		b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
			
		||||
 | 
			
		||||
		if b.v6 {
 | 
			
		||||
			err := b.apiLogin6()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			err := b.apiLogin()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		err := b.apiLogin()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	} else if b.GetString("Login") != "" {
 | 
			
		||||
		b.Log.Info("Connecting using login/password (receiving)")
 | 
			
		||||
		b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
			
		||||
 | 
			
		||||
		if b.v6 {
 | 
			
		||||
			err := b.apiLogin6()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			err := b.apiLogin()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		err := b.apiLogin()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//nolint:wrapcheck
 | 
			
		||||
func (b *Bmattermost) apiLogin() error {
 | 
			
		||||
	password := b.GetString("Password")
 | 
			
		||||
	if b.GetString("Token") != "" {
 | 
			
		||||
		password = "token=" + b.GetString("Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"))
 | 
			
		||||
	b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"), "")
 | 
			
		||||
	if b.GetBool("debug") {
 | 
			
		||||
		b.mc.SetLogLevel("debug")
 | 
			
		||||
	}
 | 
			
		||||
@@ -118,39 +81,13 @@ func (b *Bmattermost) apiLogin() error {
 | 
			
		||||
	b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck")
 | 
			
		||||
	b.mc.NoTLS = b.GetBool("NoTLS")
 | 
			
		||||
	b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
 | 
			
		||||
	err := b.mc.Login()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	b.TeamID = b.mc.GetTeamId()
 | 
			
		||||
	go b.mc.WsReceiver()
 | 
			
		||||
	go b.mc.StatusLoop()
 | 
			
		||||
	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 {
 | 
			
		||||
	if err := b.mc.Login(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	b.TeamID = b.mc6.GetTeamID()
 | 
			
		||||
	b.TeamID = b.mc.GetTeamID()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -183,6 +120,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
 | 
			
		||||
	if b.GetBool("PrefixMessagesWithNick") {
 | 
			
		||||
		msg.Text = msg.Username + msg.Text
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
 | 
			
		||||
		for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
			
		||||
@@ -206,7 +144,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
 | 
			
		||||
			for _, f := range msg.Extra["file"] {
 | 
			
		||||
				fi := f.(config.FileInfo)
 | 
			
		||||
				if fi.URL != "" {
 | 
			
		||||
					msg.Text += fi.URL
 | 
			
		||||
					msg.Text += " " + fi.URL
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -233,19 +171,37 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// skipMessages returns true if this message should not be handled
 | 
			
		||||
//
 | 
			
		||||
//nolint:gocyclo,cyclop
 | 
			
		||||
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
 | 
			
		||||
	// Handle join/leave
 | 
			
		||||
	if message.Type == "system_join_leave" ||
 | 
			
		||||
		message.Type == "system_join_channel" ||
 | 
			
		||||
		message.Type == "system_leave_channel" {
 | 
			
		||||
	skipJoinMessageTypes := map[string]struct{}{
 | 
			
		||||
		"system_join_leave":          {}, // deprecated for system_add_to_channel
 | 
			
		||||
		"system_leave_channel":       {}, // deprecated for system_remove_from_channel
 | 
			
		||||
		"system_join_channel":        {},
 | 
			
		||||
		"system_add_to_channel":      {},
 | 
			
		||||
		"system_remove_from_channel": {},
 | 
			
		||||
		"system_add_to_team":         {},
 | 
			
		||||
		"system_remove_from_team":    {},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// dirty hack to efficiently check if this element is in the map without writing a contains func
 | 
			
		||||
	// can be replaced with native slice.contains with go 1.21
 | 
			
		||||
	if _, ok := skipJoinMessageTypes[message.Type]; ok {
 | 
			
		||||
		if b.GetBool("nosendjoinpart") {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		channelName := b.getChannelName(message.Post.ChannelId)
 | 
			
		||||
		if channelName == "" {
 | 
			
		||||
			channelName = message.Channel
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
 | 
			
		||||
		b.Remote <- config.Message{
 | 
			
		||||
			Username: "system",
 | 
			
		||||
			Text:     message.Text,
 | 
			
		||||
			Channel:  message.Channel,
 | 
			
		||||
			Channel:  channelName,
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			Event:    config.EventJoinLeave,
 | 
			
		||||
		}
 | 
			
		||||
@@ -253,7 +209,7 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle edited messages
 | 
			
		||||
	if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") {
 | 
			
		||||
	if (message.Raw.EventType() == model.WebsocketEventPostEdited) && b.GetBool("EditDisable") {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -266,76 +222,14 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
 | 
			
		||||
	// Ignore messages sent from matterbridge
 | 
			
		||||
	if message.Post.Props != nil {
 | 
			
		||||
		if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
 | 
			
		||||
			b.Log.Debugf("sent by matterbridge, ignoring")
 | 
			
		||||
			b.Log.Debug("sent by matterbridge, ignoring")
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ignore messages sent from a user logged in as the bot
 | 
			
		||||
	if b.mc.User.Username == message.Username {
 | 
			
		||||
		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.Data["team_id"].(string) != b.TeamID {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// only handle posted, edited or deleted events
 | 
			
		||||
	if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED ||
 | 
			
		||||
		message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// skipMessages returns true if this message should not be handled
 | 
			
		||||
// nolint:gocyclo,cyclop
 | 
			
		||||
func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool {
 | 
			
		||||
	// Handle join/leave
 | 
			
		||||
	if message.Type == "system_join_leave" ||
 | 
			
		||||
		message.Type == "system_join_channel" ||
 | 
			
		||||
		message.Type == "system_leave_channel" {
 | 
			
		||||
		if b.GetBool("nosendjoinpart") {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
		b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
 | 
			
		||||
		b.Remote <- config.Message{
 | 
			
		||||
			Username: "system",
 | 
			
		||||
			Text:     message.Text,
 | 
			
		||||
			Channel:  message.Channel,
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			Event:    config.EventJoinLeave,
 | 
			
		||||
		}
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle edited messages
 | 
			
		||||
	if (message.Raw.EventType() == model6.WebsocketEventPostEdited) && b.GetBool("EditDisable") {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ignore non-post messages
 | 
			
		||||
	if message.Post == nil {
 | 
			
		||||
		b.Log.Debugf("ignoring nil message.Post: %#v", message)
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ignore messages sent from matterbridge
 | 
			
		||||
	if message.Post.Props != nil {
 | 
			
		||||
		if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
 | 
			
		||||
			b.Log.Debugf("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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -346,12 +240,13 @@ func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool {
 | 
			
		||||
 | 
			
		||||
	// 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) {
 | 
			
		||||
	if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model.WebsocketEventPostEdited ||
 | 
			
		||||
		message.Raw.EventType() == model.WebsocketEventPostDeleted) {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
@@ -374,3 +269,26 @@ func (b *Bmattermost) getVersion() string {
 | 
			
		||||
 | 
			
		||||
	return resp.Header.Get("X-Version-Id")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmattermost) getChannelID(name string) string {
 | 
			
		||||
	idcheck := strings.Split(name, "ID:")
 | 
			
		||||
	if len(idcheck) > 1 {
 | 
			
		||||
		return idcheck[1]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b.mc.GetChannelID(name, b.TeamID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmattermost) getChannelName(id string) string {
 | 
			
		||||
	b.channelsMutex.RLock()
 | 
			
		||||
	defer b.channelsMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	for _, c := range b.channelInfoMap {
 | 
			
		||||
		if c.Name == "ID:"+id {
 | 
			
		||||
			// if we have ID: specified in our gateway configuration return this
 | 
			
		||||
			return c.Name
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,34 +1,40 @@
 | 
			
		||||
package bmattermost
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterclient"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterhook"
 | 
			
		||||
	matterclient6 "github.com/matterbridge/matterclient"
 | 
			
		||||
	"github.com/matterbridge/matterclient"
 | 
			
		||||
	"github.com/rs/xid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Bmattermost struct {
 | 
			
		||||
	mh     *matterhook.Client
 | 
			
		||||
	mc     *matterclient.MMClient
 | 
			
		||||
	mc6    *matterclient6.Client
 | 
			
		||||
	mc     *matterclient.Client
 | 
			
		||||
	v6     bool
 | 
			
		||||
	uuid   string
 | 
			
		||||
	TeamID string
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
	avatarMap map[string]string
 | 
			
		||||
	avatarMap      map[string]string
 | 
			
		||||
	channelsMutex  sync.RWMutex
 | 
			
		||||
	channelInfoMap map[string]*config.ChannelInfo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mattermostPlugin = "mattermost.plugin"
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	b := &Bmattermost{Config: cfg, avatarMap: make(map[string]string)}
 | 
			
		||||
	b := &Bmattermost{
 | 
			
		||||
		Config:         cfg,
 | 
			
		||||
		avatarMap:      make(map[string]string),
 | 
			
		||||
		channelInfoMap: make(map[string]*config.ChannelInfo),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.v6 = b.GetBool("v6")
 | 
			
		||||
	b.uuid = xid.New().String()
 | 
			
		||||
@@ -45,7 +51,7 @@ func (b *Bmattermost) Connect() error {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.HasPrefix(b.getVersion(), "6.") {
 | 
			
		||||
	if strings.HasPrefix(b.getVersion(), "6.") || strings.HasPrefix(b.getVersion(), "7.") {
 | 
			
		||||
		if !b.v6 {
 | 
			
		||||
			b.v6 = true
 | 
			
		||||
		}
 | 
			
		||||
@@ -67,34 +73,17 @@ func (b *Bmattermost) Connect() error {
 | 
			
		||||
		return nil
 | 
			
		||||
	case b.GetString("Token") != "":
 | 
			
		||||
		b.Log.Info("Connecting using token (sending and receiving)")
 | 
			
		||||
		b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
			
		||||
 | 
			
		||||
		if b.v6 {
 | 
			
		||||
			err := b.apiLogin6()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			err := b.apiLogin()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		err := b.apiLogin()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		go b.handleMatter()
 | 
			
		||||
	case b.GetString("Login") != "":
 | 
			
		||||
		b.Log.Info("Connecting using login/password (sending and receiving)")
 | 
			
		||||
		b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
			
		||||
 | 
			
		||||
		if b.v6 {
 | 
			
		||||
			err := b.apiLogin6()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			err := b.apiLogin()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		err := b.apiLogin()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		go b.handleMatter()
 | 
			
		||||
	}
 | 
			
		||||
@@ -113,24 +102,21 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	if b.Account == mattermostPlugin {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.channelsMutex.Lock()
 | 
			
		||||
	b.channelInfoMap[channel.ID] = &channel
 | 
			
		||||
	b.channelsMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	// we can only join channels using the API
 | 
			
		||||
	if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" {
 | 
			
		||||
		var id string
 | 
			
		||||
		if b.mc6 != nil {
 | 
			
		||||
			id = b.mc6.GetChannelID(channel.Name, b.TeamID)
 | 
			
		||||
		} else {
 | 
			
		||||
			id = b.mc.GetChannelId(channel.Name, b.TeamID)
 | 
			
		||||
		}
 | 
			
		||||
		id := b.getChannelID(channel.Name)
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.mc6 != nil {
 | 
			
		||||
			return b.mc6.JoinChannel(id) // nolint:wrapcheck
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return b.mc.JoinChannel(id)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -160,9 +146,6 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
 | 
			
		||||
		if msg.ID == "" {
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
		if b.mc6 != nil {
 | 
			
		||||
			return msg.ID, b.mc6.DeleteMessage(msg.ID) // nolint:wrapcheck
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return msg.ID, b.mc.DeleteMessage(msg.ID)
 | 
			
		||||
	}
 | 
			
		||||
@@ -175,17 +158,11 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
 | 
			
		||||
 | 
			
		||||
	// we only can reply to the root of the thread, not to a specific ID (like discord for example does)
 | 
			
		||||
	if msg.ParentID != "" {
 | 
			
		||||
		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)
 | 
			
		||||
			}
 | 
			
		||||
			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)
 | 
			
		||||
			}
 | 
			
		||||
		post, _, err := b.mc.Client.GetPost(context.TODO(), msg.ParentID, "")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
 | 
			
		||||
		}
 | 
			
		||||
		if post != nil && post.RootId != "" {
 | 
			
		||||
			msg.ParentID = post.RootId
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -193,14 +170,8 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
 | 
			
		||||
	// Upload a file if it exists
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
			
		||||
			if b.mc6 != nil {
 | 
			
		||||
				if _, err := b.mc6.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
 | 
			
		||||
					b.Log.Errorf("PostMessage failed: %s", err)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
 | 
			
		||||
					b.Log.Errorf("PostMessage failed: %s", err)
 | 
			
		||||
				}
 | 
			
		||||
			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 {
 | 
			
		||||
@@ -215,17 +186,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
 | 
			
		||||
 | 
			
		||||
	// Edit message if we have an 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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post normal message
 | 
			
		||||
	if b.mc6 != nil {
 | 
			
		||||
		return b.mc6.PostMessage(b.mc6.GetChannelID(msg.Channel, b.TeamID), msg.Text, msg.ParentID) // nolint:wrapcheck
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID)
 | 
			
		||||
	return b.mc.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								bridge/mumble/codec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								bridge/mumble/codec.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
package bmumble
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"layeh.com/gumble/gumble"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// This is a dummy implementation of a Gumble audio codec which claims
 | 
			
		||||
// to implement Opus, but does not actually do anything.  This serves
 | 
			
		||||
// as a workaround until https://github.com/layeh/gumble/pull/61 is
 | 
			
		||||
// merged.
 | 
			
		||||
// See https://github.com/42wim/matterbridge/issues/1750 for details.
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	audioCodecIDOpus = 4
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func registerNullCodecAsOpus() {
 | 
			
		||||
	codec := &NullCodec{
 | 
			
		||||
		encoder: &NullAudioEncoder{},
 | 
			
		||||
		decoder: &NullAudioDecoder{},
 | 
			
		||||
	}
 | 
			
		||||
	gumble.RegisterAudioCodec(audioCodecIDOpus, codec)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NullCodec struct {
 | 
			
		||||
	encoder *NullAudioEncoder
 | 
			
		||||
	decoder *NullAudioDecoder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *NullCodec) ID() int {
 | 
			
		||||
	return audioCodecIDOpus
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *NullCodec) NewEncoder() gumble.AudioEncoder {
 | 
			
		||||
	e := &NullAudioEncoder{}
 | 
			
		||||
	return e
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *NullCodec) NewDecoder() gumble.AudioDecoder {
 | 
			
		||||
	d := &NullAudioDecoder{}
 | 
			
		||||
	return d
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NullAudioEncoder struct{}
 | 
			
		||||
 | 
			
		||||
func (e *NullAudioEncoder) ID() int {
 | 
			
		||||
	return audioCodecIDOpus
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *NullAudioEncoder) Encode(pcm []int16, mframeSize, maxDataBytes int) ([]byte, error) {
 | 
			
		||||
	return nil, fmt.Errorf("not implemented")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *NullAudioEncoder) Reset() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NullAudioDecoder struct{}
 | 
			
		||||
 | 
			
		||||
func (d *NullAudioDecoder) ID() int {
 | 
			
		||||
	return audioCodecIDOpus
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *NullAudioDecoder) Decode(data []byte, frameSize int) ([]int16, error) {
 | 
			
		||||
	return nil, fmt.Errorf("not implemented")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *NullAudioDecoder) Reset() {
 | 
			
		||||
}
 | 
			
		||||
@@ -42,7 +42,14 @@ func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
 | 
			
		||||
		if part.Image == nil {
 | 
			
		||||
			rmsg.Text = part.Text
 | 
			
		||||
		} else {
 | 
			
		||||
			fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + part.FileExtension
 | 
			
		||||
			fileExt := part.FileExtension
 | 
			
		||||
			if fileExt == ".jfif" {
 | 
			
		||||
				fileExt = ".jpg"
 | 
			
		||||
			}
 | 
			
		||||
			if fileExt == ".jpe" {
 | 
			
		||||
				fileExt = ".jpg"
 | 
			
		||||
			}
 | 
			
		||||
			fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + fileExt
 | 
			
		||||
			rmsg.Extra = make(map[string][]interface{})
 | 
			
		||||
			if err = helper.HandleDownloadSize(b.Log, &rmsg, fname, int64(len(part.Image)), b.General); err != nil {
 | 
			
		||||
				b.Log.WithError(err).Warn("not including image in message")
 | 
			
		||||
@@ -62,7 +69,6 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
 | 
			
		||||
	}
 | 
			
		||||
	// No need to talk or listen
 | 
			
		||||
	event.Client.Self.SetSelfDeafened(true)
 | 
			
		||||
	event.Client.Self.SetSelfMuted(true)
 | 
			
		||||
	// if the Channel variable is set, this is a reconnect -> rejoin channel
 | 
			
		||||
	if b.Channel != nil {
 | 
			
		||||
		if err := b.doJoin(event.Client, *b.Channel); err != nil {
 | 
			
		||||
@@ -78,19 +84,75 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) {
 | 
			
		||||
	// Only care about changes to self
 | 
			
		||||
	if event.User != event.Client.Self {
 | 
			
		||||
func (b *Bmumble) handleJoinLeave(event *gumble.UserChangeEvent) {
 | 
			
		||||
	// Ignore events happening before setup is done
 | 
			
		||||
	if b.Channel == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// Someone attempted to move the user out of the configured channel; attempt to join back
 | 
			
		||||
	if b.Channel != nil {
 | 
			
		||||
	if b.GetBool("nosendjoinpart") {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("Received gumble user change event: %+v", event)
 | 
			
		||||
 | 
			
		||||
	text := ""
 | 
			
		||||
	switch {
 | 
			
		||||
	case event.Type&gumble.UserChangeKicked > 0:
 | 
			
		||||
		text = " was kicked"
 | 
			
		||||
	case event.Type&gumble.UserChangeBanned > 0:
 | 
			
		||||
		text = " was banned"
 | 
			
		||||
	case event.Type&gumble.UserChangeDisconnected > 0:
 | 
			
		||||
		if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
 | 
			
		||||
			text = " left"
 | 
			
		||||
		}
 | 
			
		||||
	case event.Type&gumble.UserChangeConnected > 0:
 | 
			
		||||
		if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
 | 
			
		||||
			text = " joined"
 | 
			
		||||
		}
 | 
			
		||||
	case event.Type&gumble.UserChangeChannel > 0:
 | 
			
		||||
		// Treat Mumble channel changes the same as connects/disconnects; as far as matterbridge is concerned, they are identical
 | 
			
		||||
		if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
 | 
			
		||||
			text = " joined"
 | 
			
		||||
		} else {
 | 
			
		||||
			text = " left"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if text != "" {
 | 
			
		||||
		b.Remote <- config.Message{
 | 
			
		||||
			Username: "system",
 | 
			
		||||
			Text:     event.User.Name + text,
 | 
			
		||||
			Channel:  strconv.FormatUint(uint64(*b.Channel), 10),
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			Event:    config.EventJoinLeave,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleUserModified(event *gumble.UserChangeEvent) {
 | 
			
		||||
	// Ignore events happening before setup is done
 | 
			
		||||
	if b.Channel == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if event.Type&gumble.UserChangeChannel > 0 {
 | 
			
		||||
		// Someone attempted to move the user out of the configured channel; attempt to join back
 | 
			
		||||
		if err := b.doJoin(event.Client, *b.Channel); err != nil {
 | 
			
		||||
			b.Log.Error(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) {
 | 
			
		||||
	// The UserChangeEvent is used for both the gumble client itself as well as other clients
 | 
			
		||||
	if event.User != event.Client.Self {
 | 
			
		||||
		// other users
 | 
			
		||||
		b.handleJoinLeave(event)
 | 
			
		||||
	} else {
 | 
			
		||||
		// gumble user
 | 
			
		||||
		b.handleUserModified(event)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) {
 | 
			
		||||
	b.connected <- *event
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -93,7 +93,7 @@ func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
func (b *Bmumble) Send(msg config.Message) (string, error) {
 | 
			
		||||
	// Only process text messages
 | 
			
		||||
	b.Log.Debugf("=> Received local message %#v", msg)
 | 
			
		||||
	if msg.Event != "" && msg.Event != config.EventUserAction {
 | 
			
		||||
	if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -185,6 +185,7 @@ func (b *Bmumble) doConnect() error {
 | 
			
		||||
		gumbleConfig.Password = password
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	registerNullCodecAsOpus()
 | 
			
		||||
	client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -249,7 +250,12 @@ func (b *Bmumble) processMessage(msg *config.Message) {
 | 
			
		||||
	// If there is a maximum message length, split and truncate the lines
 | 
			
		||||
	var msgLines []string
 | 
			
		||||
	if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
 | 
			
		||||
		msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
 | 
			
		||||
		if *maxLength != 0 { // Some servers will have unlimited message lengths.
 | 
			
		||||
			// Not doing this makes underflows happen.
 | 
			
		||||
			msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
 | 
			
		||||
		} else {
 | 
			
		||||
			msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -135,6 +135,7 @@ func (b *Brocketchat) uploadFile(fi *config.FileInfo, channel string) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	body, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,8 @@ func (b *Bslack) handleSlack() {
 | 
			
		||||
	b.Log.Debug("Start listening for Slack messages")
 | 
			
		||||
	for message := range messages {
 | 
			
		||||
		// don't do any action on deleted/typing messages
 | 
			
		||||
		if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete {
 | 
			
		||||
		if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete &&
 | 
			
		||||
			message.Event != config.EventFileDelete {
 | 
			
		||||
			b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
 | 
			
		||||
			// cleanup the message
 | 
			
		||||
			message.Text = b.replaceMention(message.Text)
 | 
			
		||||
@@ -76,6 +77,13 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			messages <- rmsg
 | 
			
		||||
		case *slack.FileDeletedEvent:
 | 
			
		||||
			rmsg, err := b.handleFileDeletedEvent(ev)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Printf("%#v", err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			messages <- rmsg
 | 
			
		||||
		case *slack.OutgoingErrorEvent:
 | 
			
		||||
			b.Log.Debugf("%#v", ev.Error())
 | 
			
		||||
		case *slack.ChannelJoinedEvent:
 | 
			
		||||
@@ -222,6 +230,26 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
 | 
			
		||||
	return rmsg, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bslack) handleFileDeletedEvent(ev *slack.FileDeletedEvent) (*config.Message, error) {
 | 
			
		||||
	if rawChannel, ok := b.cache.Get(cfileDownloadChannel + ev.FileID); ok {
 | 
			
		||||
		channel, err := b.channels.getChannelByID(rawChannel.(string))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return &config.Message{
 | 
			
		||||
			Event:    config.EventFileDelete,
 | 
			
		||||
			Text:     config.EventFileDelete,
 | 
			
		||||
			Channel:  channel.Name,
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			ID:       ev.FileID,
 | 
			
		||||
			Protocol: b.Protocol,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, fmt.Errorf("channel ID for file ID %s not found", ev.FileID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
 | 
			
		||||
	switch ev.SubType {
 | 
			
		||||
	case sChannelJoined, sMemberJoined:
 | 
			
		||||
@@ -254,6 +282,13 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getMessageTitle(attach *slack.Attachment) string {
 | 
			
		||||
	if attach.TitleLink != "" {
 | 
			
		||||
		return fmt.Sprintf("[%s](%s)\n", attach.Title, attach.TitleLink)
 | 
			
		||||
	}
 | 
			
		||||
	return attach.Title
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) {
 | 
			
		||||
	// File comments are set by the system (because there is no username given).
 | 
			
		||||
	if ev.SubType == sFileComment {
 | 
			
		||||
@@ -262,12 +297,15 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
 | 
			
		||||
 | 
			
		||||
	// See if we have some text in the attachments.
 | 
			
		||||
	if rmsg.Text == "" {
 | 
			
		||||
		for _, attach := range ev.Attachments {
 | 
			
		||||
		for i, attach := range ev.Attachments {
 | 
			
		||||
			if attach.Text != "" {
 | 
			
		||||
				if attach.Title != "" {
 | 
			
		||||
					rmsg.Text = attach.Title + "\n"
 | 
			
		||||
					rmsg.Text = getMessageTitle(&ev.Attachments[i])
 | 
			
		||||
				}
 | 
			
		||||
				rmsg.Text += attach.Text
 | 
			
		||||
				if attach.Footer != "" {
 | 
			
		||||
					rmsg.Text += "\n\n" + attach.Footer
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				rmsg.Text = attach.Fallback
 | 
			
		||||
			}
 | 
			
		||||
@@ -281,6 +319,8 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
 | 
			
		||||
 | 
			
		||||
	// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
 | 
			
		||||
	for i := range ev.Files {
 | 
			
		||||
		// keep reference in cache on which channel we added this file
 | 
			
		||||
		b.cache.Add(cfileDownloadChannel+ev.Files[i].ID, ev.Channel)
 | 
			
		||||
		if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
 | 
			
		||||
			b.Log.Errorf("Could not download incoming file: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
@@ -330,7 +370,7 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retr
 | 
			
		||||
	// that the comment is not duplicated.
 | 
			
		||||
	comment := rmsg.Text
 | 
			
		||||
	rmsg.Text = ""
 | 
			
		||||
	helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General)
 | 
			
		||||
	helper.HandleDownloadData2(b.Log, rmsg, file.Name, file.ID, comment, file.URLPrivateDownload, data, b.General)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -87,6 +87,9 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi
 | 
			
		||||
	if user.Profile.DisplayName != "" {
 | 
			
		||||
		rmsg.Username = user.Profile.DisplayName
 | 
			
		||||
	}
 | 
			
		||||
	if b.GetBool("UseFullName") && user.Profile.RealName != "" {
 | 
			
		||||
		rmsg.Username = user.Profile.RealName
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -98,7 +101,9 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
 | 
			
		||||
	var err error
 | 
			
		||||
	var bot *slack.Bot
 | 
			
		||||
	for {
 | 
			
		||||
		bot, err = b.rtm.GetBotInfo(ev.BotID)
 | 
			
		||||
		bot, err = b.rtm.GetBotInfo(slack.GetBotInfoParameters{
 | 
			
		||||
			Bot: ev.BotID,
 | 
			
		||||
		})
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
@@ -124,7 +129,7 @@ var (
 | 
			
		||||
	mentionRE        = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
 | 
			
		||||
	channelRE        = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
 | 
			
		||||
	variableRE       = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
 | 
			
		||||
	urlRE            = regexp.MustCompile(`<(.*?)(\|.*?)?>`)
 | 
			
		||||
	urlRE            = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
 | 
			
		||||
	codeFenceRE      = regexp.MustCompile(`(?m)^` + "```" + `\w+$`)
 | 
			
		||||
	topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`)
 | 
			
		||||
)
 | 
			
		||||
@@ -178,14 +183,7 @@ func (b *Bslack) replaceVariable(text string) string {
 | 
			
		||||
 | 
			
		||||
// @see https://api.slack.com/docs/message-formatting#linking_to_urls
 | 
			
		||||
func (b *Bslack) replaceURL(text string) string {
 | 
			
		||||
	for _, r := range urlRE.FindAllStringSubmatch(text, -1) {
 | 
			
		||||
		if len(strings.TrimSpace(r[2])) == 1 { // A display text separator was found, but the text was blank
 | 
			
		||||
			text = strings.Replace(text, r[0], "", 1)
 | 
			
		||||
		} else {
 | 
			
		||||
			text = strings.Replace(text, r[0], r[1], 1)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return text
 | 
			
		||||
	return urlRE.ReplaceAllString(text, "[${2}](${1})")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bslack) replaceb0rkedMarkDown(text string) string {
 | 
			
		||||
 
 | 
			
		||||
@@ -36,24 +36,25 @@ type Bslack struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	sHello           = "hello"
 | 
			
		||||
	sChannelJoin     = "channel_join"
 | 
			
		||||
	sChannelLeave    = "channel_leave"
 | 
			
		||||
	sChannelJoined   = "channel_joined"
 | 
			
		||||
	sMemberJoined    = "member_joined_channel"
 | 
			
		||||
	sMessageChanged  = "message_changed"
 | 
			
		||||
	sMessageDeleted  = "message_deleted"
 | 
			
		||||
	sSlackAttachment = "slack_attachment"
 | 
			
		||||
	sPinnedItem      = "pinned_item"
 | 
			
		||||
	sUnpinnedItem    = "unpinned_item"
 | 
			
		||||
	sChannelTopic    = "channel_topic"
 | 
			
		||||
	sChannelPurpose  = "channel_purpose"
 | 
			
		||||
	sFileComment     = "file_comment"
 | 
			
		||||
	sMeMessage       = "me_message"
 | 
			
		||||
	sUserTyping      = "user_typing"
 | 
			
		||||
	sLatencyReport   = "latency_report"
 | 
			
		||||
	sSystemUser      = "system"
 | 
			
		||||
	sSlackBotUser    = "slackbot"
 | 
			
		||||
	sHello               = "hello"
 | 
			
		||||
	sChannelJoin         = "channel_join"
 | 
			
		||||
	sChannelLeave        = "channel_leave"
 | 
			
		||||
	sChannelJoined       = "channel_joined"
 | 
			
		||||
	sMemberJoined        = "member_joined_channel"
 | 
			
		||||
	sMessageChanged      = "message_changed"
 | 
			
		||||
	sMessageDeleted      = "message_deleted"
 | 
			
		||||
	sSlackAttachment     = "slack_attachment"
 | 
			
		||||
	sPinnedItem          = "pinned_item"
 | 
			
		||||
	sUnpinnedItem        = "unpinned_item"
 | 
			
		||||
	sChannelTopic        = "channel_topic"
 | 
			
		||||
	sChannelPurpose      = "channel_purpose"
 | 
			
		||||
	sFileComment         = "file_comment"
 | 
			
		||||
	sMeMessage           = "me_message"
 | 
			
		||||
	sUserTyping          = "user_typing"
 | 
			
		||||
	sLatencyReport       = "latency_report"
 | 
			
		||||
	sSystemUser          = "system"
 | 
			
		||||
	sSlackBotUser        = "slackbot"
 | 
			
		||||
	cfileDownloadChannel = "file_download_channel"
 | 
			
		||||
 | 
			
		||||
	tokenConfig           = "Token"
 | 
			
		||||
	incomingWebhookConfig = "WebhookBindAddress"
 | 
			
		||||
@@ -320,7 +321,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Upload a file if it exists.
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
	if len(msg.Extra) > 0 {
 | 
			
		||||
		extraMsgs := helper.HandleExtra(&msg, b.General)
 | 
			
		||||
		for i := range extraMsgs {
 | 
			
		||||
			rmsg := &extraMsgs[i]
 | 
			
		||||
@@ -331,7 +332,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// Upload files if necessary (from Slack, Telegram or Mattermost).
 | 
			
		||||
		b.uploadFile(&msg, channelInfo.ID)
 | 
			
		||||
		return b.uploadFile(&msg, channelInfo.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post message.
 | 
			
		||||
@@ -442,7 +443,8 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// uploadFile handles native upload of files
 | 
			
		||||
func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
 | 
			
		||||
func (b *Bslack) uploadFile(msg *config.Message, channelID string) (string, error) {
 | 
			
		||||
	var messageID string
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi, ok := f.(config.FileInfo)
 | 
			
		||||
		if !ok {
 | 
			
		||||
@@ -470,13 +472,22 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("uploadfile %#v", err)
 | 
			
		||||
			return
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		if res.ID != "" {
 | 
			
		||||
			b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String())
 | 
			
		||||
			b.cache.Add("file"+res.ID, ts)
 | 
			
		||||
 | 
			
		||||
			// search for message id by uploaded file in private/public channels, get thread timestamp from uploaded file
 | 
			
		||||
			if v, ok := res.Shares.Private[channelID]; ok && len(v) > 0 {
 | 
			
		||||
				messageID = v[0].Ts
 | 
			
		||||
			}
 | 
			
		||||
			if v, ok := res.Shares.Public[channelID]; ok && len(v) > 0 {
 | 
			
		||||
				messageID = v[0].Ts
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return messageID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
 | 
			
		||||
 
 | 
			
		||||
@@ -291,6 +291,7 @@ func (b *channels) populateChannels(wait bool) {
 | 
			
		||||
	queryParams := &slack.GetConversationsParameters{
 | 
			
		||||
		ExcludeArchived: true,
 | 
			
		||||
		Types:           []string{"public_channel,private_channel"},
 | 
			
		||||
		Limit:           1000,
 | 
			
		||||
	}
 | 
			
		||||
	for {
 | 
			
		||||
		channels, nextCursor, err := b.sc.GetConversations(queryParams)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package btelegram
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
@@ -9,14 +10,32 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
 | 
			
		||||
	"github.com/davecgh/go-spew/spew"
 | 
			
		||||
	tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
 | 
			
		||||
	// handle channels
 | 
			
		||||
	if posted != nil {
 | 
			
		||||
		message = posted
 | 
			
		||||
		rmsg.Text = message.Text
 | 
			
		||||
		if posted.Text == "/chatId" {
 | 
			
		||||
			chatID := strconv.FormatInt(posted.Chat.ID, 10)
 | 
			
		||||
 | 
			
		||||
			// Handle chat topics
 | 
			
		||||
			if posted.IsTopicMessage {
 | 
			
		||||
				chatID = chatID + "/" + strconv.Itoa(posted.MessageThreadID)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			_, err := b.Send(config.Message{
 | 
			
		||||
				Channel: chatID,
 | 
			
		||||
				Text:    fmt.Sprintf("ID of this chat: %s", chatID),
 | 
			
		||||
			})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Warnf("Unable to send chatID to %s", chatID)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			message = posted
 | 
			
		||||
			rmsg.Text = message.Text
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// edited channel message
 | 
			
		||||
@@ -43,6 +62,11 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if message.ForwardFromChat != nil && message.ForwardFrom == nil {
 | 
			
		||||
		rmsg.Text = "Forwarded from " + message.ForwardFromChat.Title + ": " + rmsg.Text
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if message.ForwardFrom == nil {
 | 
			
		||||
		rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text
 | 
			
		||||
		return
 | 
			
		||||
@@ -52,6 +76,9 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
 | 
			
		||||
	if b.GetBool("UseFirstName") {
 | 
			
		||||
		usernameForward = message.ForwardFrom.FirstName
 | 
			
		||||
	}
 | 
			
		||||
	if b.GetBool("UseFullName") {
 | 
			
		||||
		usernameForward = message.ForwardFrom.FirstName + " " + message.ForwardFrom.LastName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if usernameForward == "" {
 | 
			
		||||
		usernameForward = message.ForwardFrom.UserName
 | 
			
		||||
@@ -69,12 +96,16 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
 | 
			
		||||
 | 
			
		||||
// handleQuoting handles quoting of previous messages
 | 
			
		||||
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
 | 
			
		||||
	if message.ReplyToMessage != nil {
 | 
			
		||||
	// Used to check if the message was a reply to the root topic
 | 
			
		||||
	if message.ReplyToMessage != nil && (!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) { //nolint:nestif
 | 
			
		||||
		usernameReply := ""
 | 
			
		||||
		if message.ReplyToMessage.From != nil {
 | 
			
		||||
			if b.GetBool("UseFirstName") {
 | 
			
		||||
				usernameReply = message.ReplyToMessage.From.FirstName
 | 
			
		||||
			}
 | 
			
		||||
			if b.GetBool("UseFullName") {
 | 
			
		||||
				usernameReply = message.ReplyToMessage.From.FirstName + " " + message.ReplyToMessage.From.LastName
 | 
			
		||||
			}
 | 
			
		||||
			if usernameReply == "" {
 | 
			
		||||
				usernameReply = message.ReplyToMessage.From.UserName
 | 
			
		||||
				if usernameReply == "" {
 | 
			
		||||
@@ -86,7 +117,11 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
 | 
			
		||||
			usernameReply = unknownUser
 | 
			
		||||
		}
 | 
			
		||||
		if !b.GetBool("QuoteDisable") {
 | 
			
		||||
			rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
 | 
			
		||||
			quote := message.ReplyToMessage.Text
 | 
			
		||||
			if quote == "" {
 | 
			
		||||
				quote = message.ReplyToMessage.Caption
 | 
			
		||||
			}
 | 
			
		||||
			rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, quote)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -94,10 +129,15 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
 | 
			
		||||
// handleUsername handles the correct setting of the username
 | 
			
		||||
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
 | 
			
		||||
	if message.From != nil {
 | 
			
		||||
		rmsg.UserID = strconv.Itoa(message.From.ID)
 | 
			
		||||
		rmsg.UserID = strconv.FormatInt(message.From.ID, 10)
 | 
			
		||||
		if b.GetBool("UseFirstName") {
 | 
			
		||||
			rmsg.Username = message.From.FirstName
 | 
			
		||||
		}
 | 
			
		||||
		if b.GetBool("UseFullName") {
 | 
			
		||||
			if message.From.FirstName != "" && message.From.LastName != "" {
 | 
			
		||||
				rmsg.Username = message.From.FirstName + " " + message.From.LastName
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if rmsg.Username == "" {
 | 
			
		||||
			rmsg.Username = message.From.UserName
 | 
			
		||||
			if rmsg.Username == "" {
 | 
			
		||||
@@ -110,6 +150,35 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if message.SenderChat != nil { //nolint:nestif
 | 
			
		||||
		rmsg.UserID = strconv.FormatInt(message.SenderChat.ID, 10)
 | 
			
		||||
		if b.GetBool("UseFirstName") {
 | 
			
		||||
			rmsg.Username = message.SenderChat.FirstName
 | 
			
		||||
		}
 | 
			
		||||
		if b.GetBool("UseFullName") {
 | 
			
		||||
			if message.SenderChat.FirstName != "" && message.SenderChat.LastName != "" {
 | 
			
		||||
				rmsg.Username = message.SenderChat.FirstName + " " + message.SenderChat.LastName
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
 | 
			
		||||
			rmsg.Username = message.SenderChat.UserName
 | 
			
		||||
 | 
			
		||||
			if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
 | 
			
		||||
				rmsg.Username = message.SenderChat.FirstName
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// only download avatars if we have a place to upload them (configured mediaserver)
 | 
			
		||||
		if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") {
 | 
			
		||||
			b.handleDownloadAvatar(message.SenderChat.ID, rmsg.Channel)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Fallback on author signature (used in "channel" type of chat)
 | 
			
		||||
	if rmsg.Username == "" && message.AuthorSignature != "" {
 | 
			
		||||
		rmsg.Username = message.AuthorSignature
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if we really didn't find a username, set it to unknown
 | 
			
		||||
	if rmsg.Username == "" {
 | 
			
		||||
		rmsg.Username = unknownUser
 | 
			
		||||
@@ -122,10 +191,16 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
 | 
			
		||||
 | 
			
		||||
		if update.Message == nil && update.ChannelPost == nil &&
 | 
			
		||||
			update.EditedMessage == nil && update.EditedChannelPost == nil {
 | 
			
		||||
			b.Log.Error("Getting nil messages, this shouldn't happen.")
 | 
			
		||||
			b.Log.Info("Received event without messages, skipping.")
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.GetInt("debuglevel") == 1 {
 | 
			
		||||
			spew.Dump(update.Message)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.handleGroupUpdate(update)
 | 
			
		||||
 | 
			
		||||
		var message *tgbotapi.Message
 | 
			
		||||
 | 
			
		||||
		rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
 | 
			
		||||
@@ -144,6 +219,19 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
 | 
			
		||||
		// set the ID's from the channel or group message
 | 
			
		||||
		rmsg.ID = strconv.Itoa(message.MessageID)
 | 
			
		||||
		rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
 | 
			
		||||
		if message.IsTopicMessage {
 | 
			
		||||
			rmsg.Channel += "/" + strconv.Itoa(message.MessageThreadID)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// preserve threading from telegram reply
 | 
			
		||||
		if message.ReplyToMessage != nil &&
 | 
			
		||||
			// Used to check if the message was a reply to the root topic
 | 
			
		||||
			(!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) {
 | 
			
		||||
			rmsg.ParentID = strconv.Itoa(message.ReplyToMessage.MessageID)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// handle entities (adding URLs)
 | 
			
		||||
		b.handleEntities(&rmsg, message)
 | 
			
		||||
 | 
			
		||||
		// handle username
 | 
			
		||||
		b.handleUsername(&rmsg, message)
 | 
			
		||||
@@ -160,14 +248,12 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
 | 
			
		||||
		// quote the previous message
 | 
			
		||||
		b.handleQuoting(&rmsg, message)
 | 
			
		||||
 | 
			
		||||
		// handle entities (adding URLs)
 | 
			
		||||
		b.handleEntities(&rmsg, message)
 | 
			
		||||
 | 
			
		||||
		if rmsg.Text != "" || len(rmsg.Extra) > 0 {
 | 
			
		||||
			rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
 | 
			
		||||
			// Comment the next line out due to avoid removing empty lines in Telegram
 | 
			
		||||
			// rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
 | 
			
		||||
			// channels don't have (always?) user information. see #410
 | 
			
		||||
			if message.From != nil {
 | 
			
		||||
				rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
 | 
			
		||||
				rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.FormatInt(message.From.ID, 10), b.General)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
 | 
			
		||||
@@ -177,45 +263,91 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) handleGroupUpdate(update tgbotapi.Update) {
 | 
			
		||||
	if msg := update.Message; msg != nil {
 | 
			
		||||
		switch {
 | 
			
		||||
		case msg.NewChatMembers != nil:
 | 
			
		||||
			b.handleUserJoin(update)
 | 
			
		||||
		case msg.LeftChatMember != nil:
 | 
			
		||||
			b.handleUserLeave(update)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) handleUserJoin(update tgbotapi.Update) {
 | 
			
		||||
	msg := update.Message
 | 
			
		||||
	for _, user := range msg.NewChatMembers {
 | 
			
		||||
		rmsg := config.Message{
 | 
			
		||||
			UserID:   strconv.FormatInt(user.ID, 10),
 | 
			
		||||
			Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me)
 | 
			
		||||
			Channel:  strconv.FormatInt(msg.Chat.ID, 10),
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			Protocol: b.Protocol,
 | 
			
		||||
			Event:    config.EventJoinLeave,
 | 
			
		||||
			Text:     "joined chat",
 | 
			
		||||
		}
 | 
			
		||||
		b.Remote <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) handleUserLeave(update tgbotapi.Update) {
 | 
			
		||||
	msg := update.Message
 | 
			
		||||
	user := msg.LeftChatMember
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   strconv.FormatInt(user.ID, 10),
 | 
			
		||||
		Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me)
 | 
			
		||||
		Channel:  strconv.FormatInt(msg.Chat.ID, 10),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Event:    config.EventJoinLeave,
 | 
			
		||||
		Text:     "left chat",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleDownloadAvatar downloads the avatar of userid from channel
 | 
			
		||||
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
 | 
			
		||||
// logs an error message if it fails
 | 
			
		||||
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
 | 
			
		||||
func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) {
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		Username: "system",
 | 
			
		||||
		Text:     "avatar",
 | 
			
		||||
		Channel:  channel,
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		UserID:   strconv.Itoa(userid),
 | 
			
		||||
		UserID:   strconv.FormatInt(userid, 10),
 | 
			
		||||
		Event:    config.EventAvatarDownload,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
 | 
			
		||||
		photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
 | 
			
		||||
	if _, ok := b.avatarMap[strconv.FormatInt(userid, 10)]; ok {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(photos.Photos) > 0 {
 | 
			
		||||
		photo := photos.Photos[0][0]
 | 
			
		||||
		url := b.getFileDirectURL(photo.FileID)
 | 
			
		||||
		name := strconv.FormatInt(userid, 10) + ".png"
 | 
			
		||||
		b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
 | 
			
		||||
 | 
			
		||||
		err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
 | 
			
		||||
			b.Log.Error(err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(photos.Photos) > 0 {
 | 
			
		||||
			photo := photos.Photos[0][0]
 | 
			
		||||
			url := b.getFileDirectURL(photo.FileID)
 | 
			
		||||
			name := strconv.Itoa(userid) + ".png"
 | 
			
		||||
			b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
 | 
			
		||||
 | 
			
		||||
			err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Error(err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			data, err := helper.DownloadFile(url)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Errorf("download %s failed %#v", url, err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
 | 
			
		||||
			b.Remote <- rmsg
 | 
			
		||||
		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
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -251,12 +383,12 @@ func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) {
 | 
			
		||||
 | 
			
		||||
// handleDownloadFile handles file download
 | 
			
		||||
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
 | 
			
		||||
	size := 0
 | 
			
		||||
	size := int64(0)
 | 
			
		||||
	var url, name, text string
 | 
			
		||||
	switch {
 | 
			
		||||
	case message.Sticker != nil:
 | 
			
		||||
		text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
 | 
			
		||||
		size = message.Sticker.FileSize
 | 
			
		||||
		size = int64(message.Sticker.FileSize)
 | 
			
		||||
	case message.Voice != nil:
 | 
			
		||||
		text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
 | 
			
		||||
		size = message.Voice.FileSize
 | 
			
		||||
@@ -272,8 +404,8 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
 | 
			
		||||
		name = message.Document.FileName
 | 
			
		||||
		text = " " + message.Document.FileName + " : " + url
 | 
			
		||||
	case message.Photo != nil:
 | 
			
		||||
		photos := *message.Photo
 | 
			
		||||
		size = photos[len(photos)-1].FileSize
 | 
			
		||||
		photos := message.Photo
 | 
			
		||||
		size = int64(photos[len(photos)-1].FileSize)
 | 
			
		||||
		text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -319,7 +451,7 @@ func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (str
 | 
			
		||||
		urlPart := strings.Split(url, "/")
 | 
			
		||||
		name = urlPart[len(urlPart)-1]
 | 
			
		||||
	}
 | 
			
		||||
	if suffix != "" && !strings.HasSuffix(name, suffix) {
 | 
			
		||||
	if suffix != "" && !strings.HasSuffix(name, suffix) && !strings.HasSuffix(name, ".webm") {
 | 
			
		||||
		name += suffix
 | 
			
		||||
	}
 | 
			
		||||
	text := " " + url
 | 
			
		||||
@@ -331,11 +463,15 @@ func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, err
 | 
			
		||||
	if msg.ID == "" {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msgid, err := strconv.Atoi(msg.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
 | 
			
		||||
 | 
			
		||||
	cfg := tgbotapi.NewDeleteMessage(chatid, msgid)
 | 
			
		||||
	_, err = b.c.Request(cfg)
 | 
			
		||||
 | 
			
		||||
	return "", err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -373,42 +509,57 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleUploadFile handles native upload of files
 | 
			
		||||
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
 | 
			
		||||
	var c tgbotapi.Chattable
 | 
			
		||||
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64, threadid int, parentID int) (string, error) {
 | 
			
		||||
	var media []interface{}
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
		file := tgbotapi.FileBytes{
 | 
			
		||||
			Name:  fi.Name,
 | 
			
		||||
			Bytes: *fi.Data,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.GetString("MessageFormat") == HTMLFormat {
 | 
			
		||||
			fi.Comment = makeHTML(html.EscapeString(fi.Comment))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch filepath.Ext(fi.Name) {
 | 
			
		||||
		case ".jpg", ".jpe", ".png":
 | 
			
		||||
			pc := tgbotapi.NewPhotoUpload(chatid, file)
 | 
			
		||||
			pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			c = pc
 | 
			
		||||
			pc := tgbotapi.NewInputMediaPhoto(file)
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			}
 | 
			
		||||
			media = append(media, pc)
 | 
			
		||||
		case ".mp4", ".m4v":
 | 
			
		||||
			vc := tgbotapi.NewVideoUpload(chatid, file)
 | 
			
		||||
			vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			c = vc
 | 
			
		||||
			vc := tgbotapi.NewInputMediaVideo(file)
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			}
 | 
			
		||||
			media = append(media, vc)
 | 
			
		||||
		case ".mp3", ".oga":
 | 
			
		||||
			ac := tgbotapi.NewAudioUpload(chatid, file)
 | 
			
		||||
			ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			c = ac
 | 
			
		||||
			ac := tgbotapi.NewInputMediaAudio(file)
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			}
 | 
			
		||||
			media = append(media, ac)
 | 
			
		||||
		case ".ogg":
 | 
			
		||||
			voc := tgbotapi.NewVoiceUpload(chatid, file)
 | 
			
		||||
			voc := tgbotapi.NewVoice(chatid, file)
 | 
			
		||||
			voc.Caption, voc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			c = voc
 | 
			
		||||
			voc.ReplyToMessageID = parentID
 | 
			
		||||
			res, err := b.c.Send(voc)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return "", err
 | 
			
		||||
			}
 | 
			
		||||
			return strconv.Itoa(res.MessageID), nil
 | 
			
		||||
		default:
 | 
			
		||||
			dc := tgbotapi.NewDocumentUpload(chatid, file)
 | 
			
		||||
			dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			c = dc
 | 
			
		||||
		}
 | 
			
		||||
		_, err := b.c.Send(c)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("file upload failed: %#v", err)
 | 
			
		||||
			dc := tgbotapi.NewInputMediaDocument(file)
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			}
 | 
			
		||||
			media = append(media, dc)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
 | 
			
		||||
	return b.sendMediaFiles(msg, chatid, threadid, parentID, media)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
 | 
			
		||||
@@ -435,21 +586,61 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa
 | 
			
		||||
	if message.Entities == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// for now only do URL replacements
 | 
			
		||||
	for _, e := range *message.Entities {
 | 
			
		||||
 | 
			
		||||
	indexMovedBy := 0
 | 
			
		||||
	prevLinkOffset := -1
 | 
			
		||||
 | 
			
		||||
	for _, e := range message.Entities {
 | 
			
		||||
 | 
			
		||||
		asRunes := utf16.Encode([]rune(rmsg.Text))
 | 
			
		||||
 | 
			
		||||
		if e.Type == "text_link" {
 | 
			
		||||
			offset := e.Offset + indexMovedBy
 | 
			
		||||
			url, err := e.ParseURL()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Errorf("entity text_link url parse failed: %s", err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			utfEncodedString := utf16.Encode([]rune(rmsg.Text))
 | 
			
		||||
			if e.Offset+e.Length > len(utfEncodedString) {
 | 
			
		||||
				b.Log.Errorf("entity length is too long %d > %d", e.Offset+e.Length, len(utfEncodedString))
 | 
			
		||||
			if offset+e.Length > len(utfEncodedString) {
 | 
			
		||||
				b.Log.Errorf("entity length is too long %d > %d", offset+e.Length, len(utfEncodedString))
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length])
 | 
			
		||||
			rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1)
 | 
			
		||||
			rmsg.Text = string(utf16.Decode(asRunes[:offset+e.Length])) + " (" + url.String() + ")" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
			
		||||
			indexMovedBy += len(url.String()) + 3
 | 
			
		||||
			prevLinkOffset = e.Offset
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if e.Offset == prevLinkOffset {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if e.Type == "code" {
 | 
			
		||||
			offset := e.Offset + indexMovedBy
 | 
			
		||||
			rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "`" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "`" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
			
		||||
			indexMovedBy += 2
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if e.Type == "pre" {
 | 
			
		||||
			offset := e.Offset + indexMovedBy
 | 
			
		||||
			rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "```\n" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "```\n" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
			
		||||
			indexMovedBy += 8
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if e.Type == "bold" {
 | 
			
		||||
			offset := e.Offset + indexMovedBy
 | 
			
		||||
			rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "*" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "*" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
			
		||||
			indexMovedBy += 2
 | 
			
		||||
		}
 | 
			
		||||
		if e.Type == "italic" {
 | 
			
		||||
			offset := e.Offset + indexMovedBy
 | 
			
		||||
			rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "_" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "_" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
			
		||||
			indexMovedBy += 2
 | 
			
		||||
		}
 | 
			
		||||
		if e.Type == "strike" {
 | 
			
		||||
			offset := e.Offset + indexMovedBy
 | 
			
		||||
			rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "~" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "~" + string(utf16.Decode(asRunes[offset+e.Length:]))
 | 
			
		||||
			indexMovedBy += 2
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ package btelegram
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"html"
 | 
			
		||||
 | 
			
		||||
	"github.com/russross/blackfriday"
 | 
			
		||||
)
 | 
			
		||||
@@ -24,10 +23,16 @@ func (options *customHTML) Paragraph(out *bytes.Buffer, text func() bool) {
 | 
			
		||||
func (options *customHTML) BlockCode(out *bytes.Buffer, text []byte, lang string) {
 | 
			
		||||
	out.WriteString("<pre>")
 | 
			
		||||
 | 
			
		||||
	out.WriteString(html.EscapeString(string(text)))
 | 
			
		||||
	out.WriteString(string(text))
 | 
			
		||||
	out.WriteString("</pre>\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (options *customHTML) CodeSpan(out *bytes.Buffer, text []byte) {
 | 
			
		||||
	out.WriteString("<code>")
 | 
			
		||||
	out.WriteString(string(text))
 | 
			
		||||
	out.WriteString("</code>")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (options *customHTML) Header(out *bytes.Buffer, text func() bool, level int, id string) {
 | 
			
		||||
	options.Paragraph(out, text)
 | 
			
		||||
}
 | 
			
		||||
@@ -42,6 +47,10 @@ func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) {
 | 
			
		||||
	out.WriteByte('\n')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (options *customHTML) LineBreak(out *bytes.Buffer) {
 | 
			
		||||
	out.WriteByte('\n')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (options *customHTML) List(out *bytes.Buffer, text func() bool, flags int) {
 | 
			
		||||
	options.Paragraph(out, text)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package btelegram
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"log"
 | 
			
		||||
	"strconv"
 | 
			
		||||
@@ -9,7 +10,7 @@ import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
 | 
			
		||||
	tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -49,11 +50,7 @@ func (b *Btelegram) Connect() error {
 | 
			
		||||
	}
 | 
			
		||||
	u := tgbotapi.NewUpdate(0)
 | 
			
		||||
	u.Timeout = 60
 | 
			
		||||
	updates, err := b.c.GetUpdatesChan(u)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Debugf("%#v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	updates := b.c.GetUpdatesChan(u)
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	go b.handleRecv(updates)
 | 
			
		||||
	return nil
 | 
			
		||||
@@ -89,11 +86,41 @@ func TGGetParseMode(b *Btelegram, username string, text string) (textout string,
 | 
			
		||||
	return textout, parsemode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) getIds(channel string) (int64, int, error) {
 | 
			
		||||
	var chatid int64
 | 
			
		||||
	topicid := 0
 | 
			
		||||
 | 
			
		||||
	// get the chatid
 | 
			
		||||
	if strings.Contains(channel, "/") { //nolint:nestif
 | 
			
		||||
		s := strings.Split(channel, "/")
 | 
			
		||||
		if len(s) < 2 {
 | 
			
		||||
			b.Log.Errorf("Invalid channel format: %#v\n", channel)
 | 
			
		||||
			return 0, 0, nil
 | 
			
		||||
		}
 | 
			
		||||
		id, err := strconv.ParseInt(s[0], 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, 0, err
 | 
			
		||||
		}
 | 
			
		||||
		chatid = id
 | 
			
		||||
		tid, err := strconv.Atoi(s[1])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, 0, err
 | 
			
		||||
		}
 | 
			
		||||
		topicid = tid
 | 
			
		||||
	} else {
 | 
			
		||||
		id, err := strconv.ParseInt(channel, 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, 0, err
 | 
			
		||||
		}
 | 
			
		||||
		chatid = id
 | 
			
		||||
	}
 | 
			
		||||
	return chatid, topicid, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) Send(msg config.Message) (string, error) {
 | 
			
		||||
	b.Log.Debugf("=> Receiving %#v", msg)
 | 
			
		||||
 | 
			
		||||
	// get the chatid
 | 
			
		||||
	chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
 | 
			
		||||
	chatid, topicid, err := b.getIds(msg.Channel)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
@@ -104,7 +131,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.GetString("MessageFormat") == HTMLFormat {
 | 
			
		||||
		msg.Text = makeHTML(msg.Text)
 | 
			
		||||
		msg.Text = makeHTML(html.EscapeString(msg.Text))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Delete message
 | 
			
		||||
@@ -112,16 +139,27 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
 | 
			
		||||
		return b.handleDelete(&msg, chatid)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle prefix hint for unthreaded messages.
 | 
			
		||||
	if msg.ParentNotFound() {
 | 
			
		||||
		msg.ParentID = ""
 | 
			
		||||
		msg.Text = fmt.Sprintf("[reply]: %s", msg.Text)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var parentID int
 | 
			
		||||
	if msg.ParentID != "" {
 | 
			
		||||
		parentID, _ = b.intParentID(msg.ParentID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Upload a file if it exists
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
			
		||||
			if _, msgErr := b.sendMessage(chatid, rmsg.Username, rmsg.Text); msgErr != nil {
 | 
			
		||||
			if _, msgErr := b.sendMessage(chatid, topicid, rmsg.Username, rmsg.Text, parentID); msgErr != nil {
 | 
			
		||||
				b.Log.Errorf("sendMessage failed: %s", msgErr)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// check if we have files to upload (from slack, telegram or mattermost)
 | 
			
		||||
		if len(msg.Extra["file"]) > 0 {
 | 
			
		||||
			b.handleUploadFile(&msg, chatid)
 | 
			
		||||
			return b.handleUploadFile(&msg, chatid, topicid, parentID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -135,7 +173,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
 | 
			
		||||
	// Ignore empty text field needs for prevent double messages from whatsapp to telegram
 | 
			
		||||
	// when sending media with text caption
 | 
			
		||||
	if msg.Text != "" {
 | 
			
		||||
		return b.sendMessage(chatid, msg.Username, msg.Text)
 | 
			
		||||
		return b.sendMessage(chatid, topicid, msg.Username, msg.Text, parentID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "", nil
 | 
			
		||||
@@ -149,10 +187,13 @@ func (b *Btelegram) getFileDirectURL(id string) string {
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) {
 | 
			
		||||
func (b *Btelegram) sendMessage(chatid int64, topicid int, username, text string, parentID int) (string, error) {
 | 
			
		||||
	m := tgbotapi.NewMessage(chatid, "")
 | 
			
		||||
	m.Text, m.ParseMode = TGGetParseMode(b, username, text)
 | 
			
		||||
 | 
			
		||||
	if topicid != 0 {
 | 
			
		||||
		m.BaseChat.MessageThreadID = topicid
 | 
			
		||||
	}
 | 
			
		||||
	m.ReplyToMessageID = parentID
 | 
			
		||||
	m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
 | 
			
		||||
 | 
			
		||||
	res, err := b.c.Send(m)
 | 
			
		||||
@@ -162,6 +203,37 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, er
 | 
			
		||||
	return strconv.Itoa(res.MessageID), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sendMediaFiles native upload media files via media group
 | 
			
		||||
func (b *Btelegram) sendMediaFiles(msg *config.Message, chatid int64, threadid int, parentID int, media []interface{}) (string, error) {
 | 
			
		||||
	if len(media) == 0 {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
	mg := tgbotapi.MediaGroupConfig{
 | 
			
		||||
		BaseChat: tgbotapi.BaseChat{
 | 
			
		||||
			ChatID:           chatid,
 | 
			
		||||
			MessageThreadID:  threadid,
 | 
			
		||||
			ChannelUsername:  msg.Username,
 | 
			
		||||
			ReplyToMessageID: parentID,
 | 
			
		||||
		},
 | 
			
		||||
		Media: media,
 | 
			
		||||
	}
 | 
			
		||||
	messages, err := b.c.SendMediaGroup(mg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	// return first message id
 | 
			
		||||
	return strconv.Itoa(messages[0].MessageID), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// intParentID return integer parent id for telegram message
 | 
			
		||||
func (b *Btelegram) intParentID(parentID string) (int, error) {
 | 
			
		||||
	pid, err := strconv.Atoi(parentID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return pid, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) {
 | 
			
		||||
	fi := msg.Extra["file"][0].(config.FileInfo)
 | 
			
		||||
	/* if we have a sha we have successfully uploaded the file to the media server,
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ type user struct {
 | 
			
		||||
 | 
			
		||||
type Bvk struct {
 | 
			
		||||
	c            *api.VK
 | 
			
		||||
	lp           *longpoll.LongPoll
 | 
			
		||||
	usernamesMap map[int]user // cache of user names and avatar URLs
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
}
 | 
			
		||||
@@ -45,23 +46,25 @@ func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
func (b *Bvk) Connect() error {
 | 
			
		||||
	b.Log.Info("Connecting")
 | 
			
		||||
	b.c = api.NewVK(b.GetString("Token"))
 | 
			
		||||
	lp, err := longpoll.NewLongPoll(b.c, b.GetInt("GroupID"))
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	b.lp, err = longpoll.NewLongPollCommunity(b.c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Debugf("%#v", err)
 | 
			
		||||
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) {
 | 
			
		||||
	b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) {
 | 
			
		||||
		b.handleMessage(obj.Message, false)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		err := lp.Run()
 | 
			
		||||
		err := b.lp.Run()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Fatal("Enable longpoll in group management")
 | 
			
		||||
			b.Log.WithError(err).Fatal("Enable longpoll in group management")
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
@@ -69,6 +72,8 @@ func (b *Bvk) Connect() error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) Disconnect() error {
 | 
			
		||||
	b.lp.Shutdown()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -218,7 +223,7 @@ func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, s
 | 
			
		||||
		}
 | 
			
		||||
		a, err := b.uploadFile(fi, peerID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Error("File upload error ", fi.Name)
 | 
			
		||||
			b.Log.WithError(err).Error("File upload error ", fi.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		attachments = append(attachments, a)
 | 
			
		||||
@@ -232,7 +237,8 @@ func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) {
 | 
			
		||||
 | 
			
		||||
	photoRE := regexp.MustCompile(".(jpg|jpe|png)$")
 | 
			
		||||
	if photoRE.MatchString(file.Name) {
 | 
			
		||||
		p, err := b.c.UploadMessagesPhoto(peerID, r)
 | 
			
		||||
		// BUG(VK): for community chat peerID=0
 | 
			
		||||
		p, err := b.c.UploadMessagesPhoto(0, r)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
// nolint:goconst
 | 
			
		||||
package bwhatsapp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
@@ -134,6 +135,7 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleImageMessage sent from WhatsApp, relay it to the brige
 | 
			
		||||
// nolint:funlen
 | 
			
		||||
func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
 | 
			
		||||
	if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -111,8 +111,7 @@ func (b *Bwhatsapp) getSenderName(senderJid string) string {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// try to reload this contact
 | 
			
		||||
	_, err := b.conn.Contacts()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
	if _, err := b.conn.Contacts(); err != nil {
 | 
			
		||||
		b.Log.Errorf("error on update of contacts: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,11 @@ type Bwhatsapp struct {
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	number := cfg.GetString(cfgNumber)
 | 
			
		||||
 | 
			
		||||
	cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
 | 
			
		||||
	cfg.Log.Warn("This bridge is deprecated and not supported anymore. Use the new multidevice whatsapp bridge")
 | 
			
		||||
	cfg.Log.Warn("See https://github.com/42wim/matterbridge#building-with-whatsapp-beta-multidevice-support for more info")
 | 
			
		||||
	cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
 | 
			
		||||
 | 
			
		||||
	if number == "" {
 | 
			
		||||
		cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
 | 
			
		||||
	}
 | 
			
		||||
@@ -293,7 +298,11 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
 | 
			
		||||
	if msg.ID != "" {
 | 
			
		||||
		b.Log.Debugf("updating message with id %s", msg.ID)
 | 
			
		||||
 | 
			
		||||
		msg.Text += " (edited)"
 | 
			
		||||
		if b.GetString("editsuffix") != "" {
 | 
			
		||||
			msg.Text += b.GetString("EditSuffix")
 | 
			
		||||
		} else {
 | 
			
		||||
			msg.Text += " (edited)"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle Upload a file
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										454
									
								
								bridge/whatsappmulti/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										454
									
								
								bridge/whatsappmulti/handlers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,454 @@
 | 
			
		||||
//go:build whatsappmulti
 | 
			
		||||
// +build whatsappmulti
 | 
			
		||||
 | 
			
		||||
package bwhatsapp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
 | 
			
		||||
	"go.mau.fi/whatsmeow/binary/proto"
 | 
			
		||||
	"go.mau.fi/whatsmeow/types"
 | 
			
		||||
	"go.mau.fi/whatsmeow/types/events"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// nolint:gocritic
 | 
			
		||||
func (b *Bwhatsapp) eventHandler(evt interface{}) {
 | 
			
		||||
	switch e := evt.(type) {
 | 
			
		||||
	case *events.Message:
 | 
			
		||||
		b.handleMessage(e)
 | 
			
		||||
	case *events.GroupInfo:
 | 
			
		||||
		b.handleGroupInfo(e)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) handleGroupInfo(event *events.GroupInfo) {
 | 
			
		||||
	b.Log.Debugf("Receiving event %#v", event)
 | 
			
		||||
 | 
			
		||||
	switch {
 | 
			
		||||
	case event.Join != nil:
 | 
			
		||||
		b.handleUserJoin(event)
 | 
			
		||||
	case event.Leave != nil:
 | 
			
		||||
		b.handleUserLeave(event)
 | 
			
		||||
	case event.Topic != nil:
 | 
			
		||||
		b.handleTopicChange(event)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) handleUserJoin(event *events.GroupInfo) {
 | 
			
		||||
	for _, joinedJid := range event.Join {
 | 
			
		||||
		senderName := b.getSenderNameFromJID(joinedJid)
 | 
			
		||||
 | 
			
		||||
		rmsg := config.Message{
 | 
			
		||||
			UserID:   joinedJid.String(),
 | 
			
		||||
			Username: senderName,
 | 
			
		||||
			Channel:  event.JID.String(),
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			Protocol: b.Protocol,
 | 
			
		||||
			Event:    config.EventJoinLeave,
 | 
			
		||||
			Text:     "joined chat",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Remote <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) handleUserLeave(event *events.GroupInfo) {
 | 
			
		||||
	for _, leftJid := range event.Leave {
 | 
			
		||||
		senderName := b.getSenderNameFromJID(leftJid)
 | 
			
		||||
 | 
			
		||||
		rmsg := config.Message{
 | 
			
		||||
			UserID:   leftJid.String(),
 | 
			
		||||
			Username: senderName,
 | 
			
		||||
			Channel:  event.JID.String(),
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			Protocol: b.Protocol,
 | 
			
		||||
			Event:    config.EventJoinLeave,
 | 
			
		||||
			Text:     "left chat",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Remote <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) handleTopicChange(event *events.GroupInfo) {
 | 
			
		||||
	msg := event.Topic
 | 
			
		||||
	senderJid := msg.TopicSetBy
 | 
			
		||||
	senderName := b.getSenderNameFromJID(senderJid)
 | 
			
		||||
 | 
			
		||||
	text := msg.Topic
 | 
			
		||||
	if text == "" {
 | 
			
		||||
		text = "removed topic"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJid.String(),
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Channel:  event.JID.String(),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Event:    config.EventTopicChange,
 | 
			
		||||
		Text:     "Topic changed: " + text,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) handleMessage(message *events.Message) {
 | 
			
		||||
	msg := message.Message
 | 
			
		||||
	switch {
 | 
			
		||||
	case msg == nil, message.Info.IsFromMe, message.Info.Timestamp.Before(b.startedAt):
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Receiving message %#v", msg)
 | 
			
		||||
 | 
			
		||||
	switch {
 | 
			
		||||
	case msg.Conversation != nil || msg.ExtendedTextMessage != nil:
 | 
			
		||||
		b.handleTextMessage(message.Info, msg)
 | 
			
		||||
	case msg.VideoMessage != nil:
 | 
			
		||||
		b.handleVideoMessage(message)
 | 
			
		||||
	case msg.AudioMessage != nil:
 | 
			
		||||
		b.handleAudioMessage(message)
 | 
			
		||||
	case msg.DocumentMessage != nil:
 | 
			
		||||
		b.handleDocumentMessage(message)
 | 
			
		||||
	case msg.ImageMessage != nil:
 | 
			
		||||
		b.handleImageMessage(message)
 | 
			
		||||
	case msg.ProtocolMessage != nil && *msg.ProtocolMessage.Type == proto.ProtocolMessage_REVOKE:
 | 
			
		||||
		b.handleDelete(msg.ProtocolMessage)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// nolint:funlen
 | 
			
		||||
func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.Message) {
 | 
			
		||||
	senderJID := messageInfo.Sender
 | 
			
		||||
	channel := messageInfo.Chat
 | 
			
		||||
 | 
			
		||||
	senderName := b.getSenderName(messageInfo)
 | 
			
		||||
 | 
			
		||||
	if msg.GetExtendedTextMessage() == nil && msg.GetConversation() == "" {
 | 
			
		||||
		b.Log.Debugf("message without text content? %#v", msg)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var text string
 | 
			
		||||
 | 
			
		||||
	// nolint:nestif
 | 
			
		||||
	if msg.GetExtendedTextMessage() == nil {
 | 
			
		||||
		text = msg.GetConversation()
 | 
			
		||||
	} else if msg.GetExtendedTextMessage().GetContextInfo() == nil {
 | 
			
		||||
		// Handle pure text message with a link preview
 | 
			
		||||
		// A pure text message with a link preview acts as an extended text message but will not contain any context info
 | 
			
		||||
		text = msg.GetExtendedTextMessage().GetText()
 | 
			
		||||
	} else {
 | 
			
		||||
		text = msg.GetExtendedTextMessage().GetText()
 | 
			
		||||
		ci := msg.GetExtendedTextMessage().GetContextInfo()
 | 
			
		||||
 | 
			
		||||
		if senderJID == (types.JID{}) && ci.Participant != nil {
 | 
			
		||||
			senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ci.MentionedJID != nil {
 | 
			
		||||
			// handle user mentions
 | 
			
		||||
			for _, mentionedJID := range ci.MentionedJID {
 | 
			
		||||
				numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
 | 
			
		||||
 | 
			
		||||
				// mentions comes as telephone numbers and we don't want to expose it to other bridges
 | 
			
		||||
				// replace it with something more meaninful to others
 | 
			
		||||
				mention := b.getSenderNotify(types.NewJID(numberAndSuffix[0], types.DefaultUserServer))
 | 
			
		||||
 | 
			
		||||
				text = strings.Replace(text, "@"+numberAndSuffix[0], "@"+mention, 1)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parentID := ""
 | 
			
		||||
	if msg.GetExtendedTextMessage() != nil {
 | 
			
		||||
		ci := msg.GetExtendedTextMessage().GetContextInfo()
 | 
			
		||||
		parentID = getParentIdFromCtx(ci)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID.String(),
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Text:     text,
 | 
			
		||||
		Channel:  channel.String(),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		ID:       getMessageIdFormat(senderJID, messageInfo.ID),
 | 
			
		||||
		ParentID: parentID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
 | 
			
		||||
		rmsg.Avatar = avatarURL
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleImageMessage sent from WhatsApp, relay it to the brige
 | 
			
		||||
func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
 | 
			
		||||
	imsg := msg.Message.GetImageMessage()
 | 
			
		||||
 | 
			
		||||
	senderJID := msg.Info.Sender
 | 
			
		||||
	senderName := b.getSenderName(msg.Info)
 | 
			
		||||
	ci := imsg.GetContextInfo()
 | 
			
		||||
 | 
			
		||||
	if senderJID == (types.JID{}) && ci.Participant != nil {
 | 
			
		||||
		senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID.String(),
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Channel:  msg.Info.Chat.String(),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		ID:       getMessageIdFormat(senderJID, msg.Info.ID),
 | 
			
		||||
		ParentID: getParentIdFromCtx(ci),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
 | 
			
		||||
		rmsg.Avatar = avatarURL
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Mimetype detection error: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
 | 
			
		||||
	if fileExt[0] == ".jfif" {
 | 
			
		||||
		fileExt[0] = ".jpg"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463
 | 
			
		||||
	if fileExt[0] == ".jpe" {
 | 
			
		||||
		fileExt[0] = ".jpg"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Trying to download %s with type %s", filename, imsg.GetMimetype())
 | 
			
		||||
 | 
			
		||||
	data, err := b.wc.Download(imsg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Download image failed: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Move file to bridge storage
 | 
			
		||||
	helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleVideoMessage downloads video messages
 | 
			
		||||
func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
 | 
			
		||||
	imsg := msg.Message.GetVideoMessage()
 | 
			
		||||
 | 
			
		||||
	senderJID := msg.Info.Sender
 | 
			
		||||
	senderName := b.getSenderName(msg.Info)
 | 
			
		||||
	ci := imsg.GetContextInfo()
 | 
			
		||||
 | 
			
		||||
	if senderJID == (types.JID{}) && ci.Participant != nil {
 | 
			
		||||
		senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID.String(),
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Channel:  msg.Info.Chat.String(),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		ID:       getMessageIdFormat(senderJID, msg.Info.ID),
 | 
			
		||||
		ParentID: getParentIdFromCtx(ci),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
 | 
			
		||||
		rmsg.Avatar = avatarURL
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Mimetype detection error: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(fileExt) == 0 {
 | 
			
		||||
		fileExt = append(fileExt, ".mp4")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prefer .mp4 extension, otherwise fallback to first index
 | 
			
		||||
	fileExtIndex := 0
 | 
			
		||||
	for i, n := range fileExt {
 | 
			
		||||
		if ".mp4" == n {
 | 
			
		||||
			fileExtIndex = i
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[fileExtIndex])
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
 | 
			
		||||
 | 
			
		||||
	data, err := b.wc.Download(imsg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Download video failed: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Move file to bridge storage
 | 
			
		||||
	helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleAudioMessage downloads audio messages
 | 
			
		||||
func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
 | 
			
		||||
	imsg := msg.Message.GetAudioMessage()
 | 
			
		||||
 | 
			
		||||
	senderJID := msg.Info.Sender
 | 
			
		||||
	senderName := b.getSenderName(msg.Info)
 | 
			
		||||
	ci := imsg.GetContextInfo()
 | 
			
		||||
 | 
			
		||||
	if senderJID == (types.JID{}) && ci.Participant != nil {
 | 
			
		||||
		senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
 | 
			
		||||
	}
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID.String(),
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Channel:  msg.Info.Chat.String(),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		ID:       getMessageIdFormat(senderJID, msg.Info.ID),
 | 
			
		||||
		ParentID: getParentIdFromCtx(ci),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
 | 
			
		||||
		rmsg.Avatar = avatarURL
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Mimetype detection error: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(fileExt) == 0 {
 | 
			
		||||
		fileExt = append(fileExt, ".ogg")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
 | 
			
		||||
 | 
			
		||||
	data, err := b.wc.Download(imsg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Download video failed: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Move file to bridge storage
 | 
			
		||||
	helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleDocumentMessage downloads documents
 | 
			
		||||
func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) {
 | 
			
		||||
	imsg := msg.Message.GetDocumentMessage()
 | 
			
		||||
 | 
			
		||||
	senderJID := msg.Info.Sender
 | 
			
		||||
	senderName := b.getSenderName(msg.Info)
 | 
			
		||||
	ci := imsg.GetContextInfo()
 | 
			
		||||
 | 
			
		||||
	if senderJID == (types.JID{}) && ci.Participant != nil {
 | 
			
		||||
		senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID.String(),
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Channel:  msg.Info.Chat.String(),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		ID:       getMessageIdFormat(senderJID, msg.Info.ID),
 | 
			
		||||
		ParentID: getParentIdFromCtx(ci),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
 | 
			
		||||
		rmsg.Avatar = avatarURL
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Mimetype detection error: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filename := fmt.Sprintf("%v", imsg.GetFileName())
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, imsg.GetMimetype())
 | 
			
		||||
 | 
			
		||||
	data, err := b.wc.Download(imsg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Download document message failed: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Move file to bridge storage
 | 
			
		||||
	helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) handleDelete(messageInfo *proto.ProtocolMessage) {
 | 
			
		||||
	sender, _ := types.ParseJID(*messageInfo.Key.Participant)
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		ID:       getMessageIdFormat(sender, *messageInfo.Key.ID),
 | 
			
		||||
		Event:    config.EventMsgDelete,
 | 
			
		||||
		Text:     config.EventMsgDelete,
 | 
			
		||||
		Channel:  *messageInfo.Key.RemoteJID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										209
									
								
								bridge/whatsappmulti/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								bridge/whatsappmulti/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,209 @@
 | 
			
		||||
//go:build whatsappmulti
 | 
			
		||||
// +build whatsappmulti
 | 
			
		||||
 | 
			
		||||
package bwhatsapp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	goproto "google.golang.org/protobuf/proto"
 | 
			
		||||
 | 
			
		||||
	"go.mau.fi/whatsmeow"
 | 
			
		||||
	"go.mau.fi/whatsmeow/binary/proto"
 | 
			
		||||
	"go.mau.fi/whatsmeow/store"
 | 
			
		||||
	"go.mau.fi/whatsmeow/store/sqlstore"
 | 
			
		||||
	"go.mau.fi/whatsmeow/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ProfilePicInfo struct {
 | 
			
		||||
	URL    string `json:"eurl"`
 | 
			
		||||
	Tag    string `json:"tag"`
 | 
			
		||||
	Status int16  `json:"status"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) reloadContacts() {
 | 
			
		||||
	if _, err := b.wc.Store.Contacts.GetAllContacts(); err != nil {
 | 
			
		||||
		b.Log.Errorf("error on update of contacts: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	allcontacts, err := b.wc.Store.Contacts.GetAllContacts()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("error on update of contacts: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(allcontacts) > 0 {
 | 
			
		||||
		b.contacts = allcontacts
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) getSenderName(info types.MessageInfo) string {
 | 
			
		||||
	// Parse AD JID
 | 
			
		||||
	var senderJid types.JID
 | 
			
		||||
	senderJid.User, senderJid.Server = info.Sender.User, info.Sender.Server
 | 
			
		||||
 | 
			
		||||
	sender, exists := b.contacts[senderJid]
 | 
			
		||||
 | 
			
		||||
	if !exists || (sender.FullName == "" && sender.FirstName == "") {
 | 
			
		||||
		b.reloadContacts() // Contacts may need to be reloaded
 | 
			
		||||
		sender, exists = b.contacts[senderJid]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if exists && sender.FullName != "" {
 | 
			
		||||
		return sender.FullName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if info.PushName != "" {
 | 
			
		||||
		return info.PushName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if exists && sender.FirstName != "" {
 | 
			
		||||
		return sender.FirstName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "Someone"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) getSenderNameFromJID(senderJid types.JID) string {
 | 
			
		||||
	sender, exists := b.contacts[senderJid]
 | 
			
		||||
 | 
			
		||||
	if !exists || (sender.FullName == "" && sender.FirstName == "") {
 | 
			
		||||
		b.reloadContacts() // Contacts may need to be reloaded
 | 
			
		||||
		sender, exists = b.contacts[senderJid]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if exists && sender.FullName != "" {
 | 
			
		||||
		return sender.FullName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if exists && sender.FirstName != "" {
 | 
			
		||||
		return sender.FirstName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if sender.PushName != "" {
 | 
			
		||||
		return sender.PushName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "Someone"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) getSenderNotify(senderJid types.JID) string {
 | 
			
		||||
	sender, exists := b.contacts[senderJid]
 | 
			
		||||
 | 
			
		||||
	if !exists || (sender.FullName == "" && sender.PushName == "" && sender.FirstName == "") {
 | 
			
		||||
		b.reloadContacts() // Contacts may need to be reloaded
 | 
			
		||||
		sender, exists = b.contacts[senderJid]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !exists {
 | 
			
		||||
		return "someone"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if exists && sender.FullName != "" {
 | 
			
		||||
		return sender.FullName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if exists && sender.PushName != "" {
 | 
			
		||||
		return sender.PushName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if exists && sender.FirstName != "" {
 | 
			
		||||
		return sender.FirstName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "someone"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*types.ProfilePictureInfo, error) {
 | 
			
		||||
	pjid, _ := types.ParseJID(jid)
 | 
			
		||||
 | 
			
		||||
	info, err := b.wc.GetProfilePictureInfo(pjid, &whatsmeow.GetProfilePictureParams{
 | 
			
		||||
		Preview: true,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to get avatar: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return info, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isGroupJid(identifier string) bool {
 | 
			
		||||
	return strings.HasSuffix(identifier, "@g.us") ||
 | 
			
		||||
		strings.HasSuffix(identifier, "@temp") ||
 | 
			
		||||
		strings.HasSuffix(identifier, "@broadcast")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) getDevice() (*store.Device, error) {
 | 
			
		||||
	device := &store.Device{}
 | 
			
		||||
 | 
			
		||||
	storeContainer, err := sqlstore.New("sqlite", "file:"+b.Config.GetString("sessionfile")+".db?_pragma=foreign_keys(1)&_pragma=busy_timeout=10000", nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return device, fmt.Errorf("failed to connect to database: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	device, err = storeContainer.GetFirstDevice()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return device, fmt.Errorf("failed to get device: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return device, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) getNewReplyContext(parentID string) (*proto.ContextInfo, error) {
 | 
			
		||||
	replyInfo, err := b.parseMessageID(parentID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sender := fmt.Sprintf("%s@%s", replyInfo.Sender.User, replyInfo.Sender.Server)
 | 
			
		||||
	ctx := &proto.ContextInfo{
 | 
			
		||||
		StanzaID:      &replyInfo.MessageID,
 | 
			
		||||
		Participant:   &sender,
 | 
			
		||||
		QuotedMessage: &proto.Message{Conversation: goproto.String("")},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ctx, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) parseMessageID(id string) (*Replyable, error) {
 | 
			
		||||
	// No message ID in case action is executed on a message sent before the bridge was started
 | 
			
		||||
	// and then the bridge cache doesn't have this message ID mapped
 | 
			
		||||
	if id == "" {
 | 
			
		||||
		return &Replyable{MessageID: id}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	replyInfo := strings.Split(id, "/")
 | 
			
		||||
 | 
			
		||||
	if len(replyInfo) == 2 {
 | 
			
		||||
		sender, err := types.ParseJID(replyInfo[0])
 | 
			
		||||
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return &Replyable{
 | 
			
		||||
				MessageID: types.MessageID(replyInfo[1]),
 | 
			
		||||
				Sender:    sender,
 | 
			
		||||
			}, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := fmt.Errorf("MessageID does not match format of {senderJID}:{messageID} : \"%s\"", id)
 | 
			
		||||
 | 
			
		||||
	return &Replyable{MessageID: id}, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getParentIdFromCtx(ci *proto.ContextInfo) string {
 | 
			
		||||
	if ci != nil && ci.StanzaID != nil {
 | 
			
		||||
		senderJid, err := types.ParseJID(*ci.Participant)
 | 
			
		||||
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return getMessageIdFormat(senderJid, *ci.StanzaID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getMessageIdFormat(jid types.JID, messageID string) string {
 | 
			
		||||
	// we're crafting our own JID str as AD JID format messes with how stuff looks on a webclient
 | 
			
		||||
	jidStr := fmt.Sprintf("%s@%s", jid.User, jid.Server)
 | 
			
		||||
	return fmt.Sprintf("%s/%s", jidStr, messageID)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										456
									
								
								bridge/whatsappmulti/whatsapp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										456
									
								
								bridge/whatsappmulti/whatsapp.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,456 @@
 | 
			
		||||
//go:build whatsappmulti
 | 
			
		||||
// +build whatsappmulti
 | 
			
		||||
 | 
			
		||||
package bwhatsapp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/mdp/qrterminal"
 | 
			
		||||
 | 
			
		||||
	"go.mau.fi/whatsmeow"
 | 
			
		||||
	"go.mau.fi/whatsmeow/binary/proto"
 | 
			
		||||
	"go.mau.fi/whatsmeow/types"
 | 
			
		||||
	waLog "go.mau.fi/whatsmeow/util/log"
 | 
			
		||||
 | 
			
		||||
	goproto "google.golang.org/protobuf/proto"
 | 
			
		||||
 | 
			
		||||
	_ "modernc.org/sqlite" // needed for sqlite
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// Account config parameters
 | 
			
		||||
	cfgNumber = "Number"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Bwhatsapp Bridge structure keeping all the information needed for relying
 | 
			
		||||
type Bwhatsapp struct {
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
 | 
			
		||||
	startedAt    time.Time
 | 
			
		||||
	wc           *whatsmeow.Client
 | 
			
		||||
	contacts     map[types.JID]types.ContactInfo
 | 
			
		||||
	users        map[string]types.ContactInfo
 | 
			
		||||
	userAvatars  map[string]string
 | 
			
		||||
	joinedGroups []*types.GroupInfo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Replyable struct {
 | 
			
		||||
	MessageID types.MessageID
 | 
			
		||||
	Sender    types.JID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	number := cfg.GetString(cfgNumber)
 | 
			
		||||
 | 
			
		||||
	if number == "" {
 | 
			
		||||
		cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b := &Bwhatsapp{
 | 
			
		||||
		Config: cfg,
 | 
			
		||||
 | 
			
		||||
		users:       make(map[string]types.ContactInfo),
 | 
			
		||||
		userAvatars: make(map[string]string),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Connect to WhatsApp. Required implementation of the Bridger interface
 | 
			
		||||
func (b *Bwhatsapp) Connect() error {
 | 
			
		||||
	device, err := b.getDevice()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	number := b.GetString(cfgNumber)
 | 
			
		||||
	if number == "" {
 | 
			
		||||
		return errors.New("whatsapp's telephone number need to be configured")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugln("Connecting to WhatsApp..")
 | 
			
		||||
 | 
			
		||||
	b.wc = whatsmeow.NewClient(device, waLog.Stdout("Client", "INFO", true))
 | 
			
		||||
	b.wc.AddEventHandler(b.eventHandler)
 | 
			
		||||
 | 
			
		||||
	firstlogin := false
 | 
			
		||||
	var qrChan <-chan whatsmeow.QRChannelItem
 | 
			
		||||
	if b.wc.Store.ID == nil {
 | 
			
		||||
		firstlogin = true
 | 
			
		||||
		qrChan, err = b.wc.GetQRChannel(context.Background())
 | 
			
		||||
		if err != nil && !errors.Is(err, whatsmeow.ErrQRStoreContainsID) {
 | 
			
		||||
			return errors.New("failed to to get QR channel:" + err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = b.wc.Connect()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.New("failed to connect to WhatsApp: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.wc.Store.ID == nil {
 | 
			
		||||
		for evt := range qrChan {
 | 
			
		||||
			if evt.Event == "code" {
 | 
			
		||||
				qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
 | 
			
		||||
			} else {
 | 
			
		||||
				b.Log.Infof("QR channel result: %s", evt.Event)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// disconnect and reconnect on our first login/pairing
 | 
			
		||||
	// for some reason the GetJoinedGroups in JoinChannel doesn't work on first login
 | 
			
		||||
	if firstlogin {
 | 
			
		||||
		b.wc.Disconnect()
 | 
			
		||||
		time.Sleep(time.Second)
 | 
			
		||||
 | 
			
		||||
		err = b.wc.Connect()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.New("failed to connect to WhatsApp: " + err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Infoln("WhatsApp connection successful")
 | 
			
		||||
 | 
			
		||||
	b.contacts, err = b.wc.Store.Contacts.GetAllContacts()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.New("failed to get contacts: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.joinedGroups, err = b.wc.GetJoinedGroups()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.New("failed to get list of joined groups: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.startedAt = time.Now()
 | 
			
		||||
 | 
			
		||||
	// map all the users
 | 
			
		||||
	for id, contact := range b.contacts {
 | 
			
		||||
		if !isGroupJid(id.String()) && id.String() != "status@broadcast" {
 | 
			
		||||
			// it is user
 | 
			
		||||
			b.users[id.String()] = contact
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// get user avatar asynchronously
 | 
			
		||||
	b.Log.Info("Getting user avatars..")
 | 
			
		||||
 | 
			
		||||
	for jid := range b.users {
 | 
			
		||||
		info, err := b.GetProfilePicThumb(jid)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
 | 
			
		||||
		} else {
 | 
			
		||||
			b.Lock()
 | 
			
		||||
			if info != nil {
 | 
			
		||||
				b.userAvatars[jid] = info.URL
 | 
			
		||||
			}
 | 
			
		||||
			b.Unlock()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Info("Finished getting avatars..")
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Disconnect is called while reconnecting to the bridge
 | 
			
		||||
// Required implementation of the Bridger interface
 | 
			
		||||
func (b *Bwhatsapp) Disconnect() error {
 | 
			
		||||
	b.wc.Disconnect()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
 | 
			
		||||
// Required implementation of the Bridger interface
 | 
			
		||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
 | 
			
		||||
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	byJid := isGroupJid(channel.Name)
 | 
			
		||||
 | 
			
		||||
	// verify if we are member of the given group
 | 
			
		||||
	if byJid {
 | 
			
		||||
		gJID, err := types.ParseJID(channel.Name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, group := range b.joinedGroups {
 | 
			
		||||
			if group.JID == gJID {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	foundGroups := []string{}
 | 
			
		||||
 | 
			
		||||
	for _, group := range b.joinedGroups {
 | 
			
		||||
		if group.Name == channel.Name {
 | 
			
		||||
			foundGroups = append(foundGroups, group.Name)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch len(foundGroups) {
 | 
			
		||||
	case 0:
 | 
			
		||||
		// didn't match any group - print out possibilites
 | 
			
		||||
		for _, group := range b.joinedGroups {
 | 
			
		||||
			b.Log.Infof("%s %s", group.JID, group.Name)
 | 
			
		||||
		}
 | 
			
		||||
		return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
 | 
			
		||||
	case 1:
 | 
			
		||||
		return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", foundGroups[0], channel.Name)
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, foundGroups)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Post a document message from the bridge to WhatsApp
 | 
			
		||||
func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
 | 
			
		||||
	groupJID, _ := types.ParseJID(msg.Channel)
 | 
			
		||||
 | 
			
		||||
	fi := msg.Extra["file"][0].(config.FileInfo)
 | 
			
		||||
 | 
			
		||||
	caption := msg.Username + fi.Comment
 | 
			
		||||
 | 
			
		||||
	resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaDocument)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post document message
 | 
			
		||||
	var message proto.Message
 | 
			
		||||
	var ctx *proto.ContextInfo
 | 
			
		||||
	if msg.ParentID != "" {
 | 
			
		||||
		ctx, _ = b.getNewReplyContext(msg.ParentID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message.DocumentMessage = &proto.DocumentMessage{
 | 
			
		||||
		Title:         &fi.Name,
 | 
			
		||||
		FileName:      &fi.Name,
 | 
			
		||||
		Mimetype:      &filetype,
 | 
			
		||||
		Caption:       &caption,
 | 
			
		||||
		MediaKey:      resp.MediaKey,
 | 
			
		||||
		FileEncSHA256: resp.FileEncSHA256,
 | 
			
		||||
		FileSHA256:    resp.FileSHA256,
 | 
			
		||||
		FileLength:    goproto.Uint64(resp.FileLength),
 | 
			
		||||
		URL:           &resp.URL,
 | 
			
		||||
		DirectPath:    &resp.DirectPath,
 | 
			
		||||
		ContextInfo:   ctx,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Sending %#v as a document", msg)
 | 
			
		||||
 | 
			
		||||
	ID := whatsmeow.GenerateMessageID()
 | 
			
		||||
	_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
 | 
			
		||||
 | 
			
		||||
	return ID, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Post an image message from the bridge to WhatsApp
 | 
			
		||||
// Handle, for sure image/jpeg, image/png and image/gif MIME types
 | 
			
		||||
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
 | 
			
		||||
	fi := msg.Extra["file"][0].(config.FileInfo)
 | 
			
		||||
 | 
			
		||||
	caption := msg.Username + fi.Comment
 | 
			
		||||
 | 
			
		||||
	resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaImage)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var message proto.Message
 | 
			
		||||
	var ctx *proto.ContextInfo
 | 
			
		||||
	if msg.ParentID != "" {
 | 
			
		||||
		ctx, _ = b.getNewReplyContext(msg.ParentID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message.ImageMessage = &proto.ImageMessage{
 | 
			
		||||
		Mimetype:      &filetype,
 | 
			
		||||
		Caption:       &caption,
 | 
			
		||||
		MediaKey:      resp.MediaKey,
 | 
			
		||||
		FileEncSHA256: resp.FileEncSHA256,
 | 
			
		||||
		FileSHA256:    resp.FileSHA256,
 | 
			
		||||
		FileLength:    goproto.Uint64(resp.FileLength),
 | 
			
		||||
		URL:           &resp.URL,
 | 
			
		||||
		DirectPath:    &resp.DirectPath,
 | 
			
		||||
		ContextInfo:   ctx,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Sending %#v as an image", msg)
 | 
			
		||||
 | 
			
		||||
	return b.sendMessage(msg, &message)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Post a video message from the bridge to WhatsApp
 | 
			
		||||
func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (string, error) {
 | 
			
		||||
	fi := msg.Extra["file"][0].(config.FileInfo)
 | 
			
		||||
 | 
			
		||||
	caption := msg.Username + fi.Comment
 | 
			
		||||
 | 
			
		||||
	resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaVideo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var message proto.Message
 | 
			
		||||
	var ctx *proto.ContextInfo
 | 
			
		||||
	if msg.ParentID != "" {
 | 
			
		||||
		ctx, _ = b.getNewReplyContext(msg.ParentID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message.VideoMessage = &proto.VideoMessage{
 | 
			
		||||
		Mimetype:      &filetype,
 | 
			
		||||
		Caption:       &caption,
 | 
			
		||||
		MediaKey:      resp.MediaKey,
 | 
			
		||||
		FileEncSHA256: resp.FileEncSHA256,
 | 
			
		||||
		FileSHA256:    resp.FileSHA256,
 | 
			
		||||
		FileLength:    goproto.Uint64(resp.FileLength),
 | 
			
		||||
		URL:           &resp.URL,
 | 
			
		||||
		DirectPath:    &resp.DirectPath,
 | 
			
		||||
		ContextInfo:   ctx,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Sending %#v as a video", msg)
 | 
			
		||||
 | 
			
		||||
	return b.sendMessage(msg, &message)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Post audio inline
 | 
			
		||||
func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (string, error) {
 | 
			
		||||
	groupJID, _ := types.ParseJID(msg.Channel)
 | 
			
		||||
 | 
			
		||||
	fi := msg.Extra["file"][0].(config.FileInfo)
 | 
			
		||||
 | 
			
		||||
	resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaAudio)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var message proto.Message
 | 
			
		||||
	var ctx *proto.ContextInfo
 | 
			
		||||
	if msg.ParentID != "" {
 | 
			
		||||
		ctx, _ = b.getNewReplyContext(msg.ParentID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message.AudioMessage = &proto.AudioMessage{
 | 
			
		||||
		Mimetype:      &filetype,
 | 
			
		||||
		MediaKey:      resp.MediaKey,
 | 
			
		||||
		FileEncSHA256: resp.FileEncSHA256,
 | 
			
		||||
		FileSHA256:    resp.FileSHA256,
 | 
			
		||||
		FileLength:    goproto.Uint64(resp.FileLength),
 | 
			
		||||
		URL:           &resp.URL,
 | 
			
		||||
		DirectPath:    &resp.DirectPath,
 | 
			
		||||
		ContextInfo:   ctx,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Sending %#v as audio", msg)
 | 
			
		||||
 | 
			
		||||
	ID, err := b.sendMessage(msg, &message)
 | 
			
		||||
 | 
			
		||||
	var captionMessage proto.Message
 | 
			
		||||
	caption := msg.Username + fi.Comment + "\u2B06" // the char on the end is upwards arrow emoji
 | 
			
		||||
	captionMessage.Conversation = &caption
 | 
			
		||||
 | 
			
		||||
	captionID := whatsmeow.GenerateMessageID()
 | 
			
		||||
	_, err = b.wc.SendMessage(context.TODO(), groupJID, &captionMessage, whatsmeow.SendRequestExtra{ID: captionID})
 | 
			
		||||
 | 
			
		||||
	return ID, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Send a message from the bridge to WhatsApp
 | 
			
		||||
func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
 | 
			
		||||
	groupJID, _ := types.ParseJID(msg.Channel)
 | 
			
		||||
 | 
			
		||||
	extendedMsgID, _ := b.parseMessageID(msg.ID)
 | 
			
		||||
	msg.ID = extendedMsgID.MessageID
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Receiving %#v", msg)
 | 
			
		||||
 | 
			
		||||
	// Delete message
 | 
			
		||||
	if msg.Event == config.EventMsgDelete {
 | 
			
		||||
		if msg.ID == "" {
 | 
			
		||||
			// No message ID in case action is executed on a message sent before the bridge was started
 | 
			
		||||
			// and then the bridge cache doesn't have this message ID mapped
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err := b.wc.RevokeMessage(groupJID, msg.ID)
 | 
			
		||||
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Edit message
 | 
			
		||||
	if msg.ID != "" {
 | 
			
		||||
		b.Log.Debugf("updating message with id %s", msg.ID)
 | 
			
		||||
 | 
			
		||||
		if b.GetString("editsuffix") != "" {
 | 
			
		||||
			msg.Text += b.GetString("EditSuffix")
 | 
			
		||||
		} else {
 | 
			
		||||
			msg.Text += " (edited)"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle Upload a file
 | 
			
		||||
	if msg.Extra["file"] != nil {
 | 
			
		||||
		fi := msg.Extra["file"][0].(config.FileInfo)
 | 
			
		||||
		filetype := mime.TypeByExtension(filepath.Ext(fi.Name))
 | 
			
		||||
 | 
			
		||||
		b.Log.Debugf("Extra file is %#v", filetype)
 | 
			
		||||
 | 
			
		||||
		// TODO: add different types
 | 
			
		||||
		// TODO: add webp conversion
 | 
			
		||||
		switch filetype {
 | 
			
		||||
		case "image/jpeg", "image/png", "image/gif":
 | 
			
		||||
			return b.PostImageMessage(msg, filetype)
 | 
			
		||||
		case "video/mp4", "video/3gpp": // TODO: Check if codecs are supported by WA
 | 
			
		||||
			return b.PostVideoMessage(msg, filetype)
 | 
			
		||||
		case "audio/ogg":
 | 
			
		||||
			return b.PostAudioMessage(msg, "audio/ogg; codecs=opus") // TODO: Detect if it is actually OPUS
 | 
			
		||||
		case "audio/aac", "audio/mp4", "audio/amr", "audio/mpeg":
 | 
			
		||||
			return b.PostAudioMessage(msg, filetype)
 | 
			
		||||
		default:
 | 
			
		||||
			return b.PostDocumentMessage(msg, filetype)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var message proto.Message
 | 
			
		||||
	text := msg.Username + msg.Text
 | 
			
		||||
 | 
			
		||||
	// If we have a parent ID send an extended message
 | 
			
		||||
	if msg.ParentID != "" {
 | 
			
		||||
		replyContext, err := b.getNewReplyContext(msg.ParentID)
 | 
			
		||||
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			message = proto.Message{
 | 
			
		||||
				ExtendedTextMessage: &proto.ExtendedTextMessage{
 | 
			
		||||
					Text:        &text,
 | 
			
		||||
					ContextInfo: replyContext,
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return b.sendMessage(msg, &message)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message.Conversation = &text
 | 
			
		||||
 | 
			
		||||
	return b.sendMessage(msg, &message)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) sendMessage(rmsg config.Message, message *proto.Message) (string, error) {
 | 
			
		||||
	groupJID, _ := types.ParseJID(rmsg.Channel)
 | 
			
		||||
	ID := whatsmeow.GenerateMessageID()
 | 
			
		||||
 | 
			
		||||
	_, err := b.wc.SendMessage(context.Background(), groupJID, message, whatsmeow.SendRequestExtra{ID: ID})
 | 
			
		||||
 | 
			
		||||
	return getMessageIdFormat(*b.wc.Store.ID, ID), err
 | 
			
		||||
}
 | 
			
		||||
@@ -14,6 +14,7 @@ import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	lru "github.com/hashicorp/golang-lru"
 | 
			
		||||
	"github.com/jpillora/backoff"
 | 
			
		||||
	"github.com/matterbridge/go-xmpp"
 | 
			
		||||
	"github.com/rs/xid"
 | 
			
		||||
@@ -28,13 +29,20 @@ type Bxmpp struct {
 | 
			
		||||
	connected bool
 | 
			
		||||
	sync.RWMutex
 | 
			
		||||
 | 
			
		||||
	StanzaIDs          *lru.Cache
 | 
			
		||||
	OriginIDs          *lru.Cache
 | 
			
		||||
 | 
			
		||||
	avatarAvailability map[string]bool
 | 
			
		||||
	avatarMap          map[string]string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	stanzaIDs, _ := lru.New(5000)
 | 
			
		||||
	originIDs, _ := lru.New(5000)
 | 
			
		||||
	return &Bxmpp{
 | 
			
		||||
		Config:             cfg,
 | 
			
		||||
		StanzaIDs:          stanzaIDs,
 | 
			
		||||
		OriginIDs:          originIDs,
 | 
			
		||||
		xmppMap:            make(map[string]string),
 | 
			
		||||
		avatarAvailability: make(map[string]bool),
 | 
			
		||||
		avatarMap:          make(map[string]string),
 | 
			
		||||
@@ -124,12 +132,20 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.ParentNotFound() {
 | 
			
		||||
		msg.ParentID = ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post normal message.
 | 
			
		||||
	var msgReplaceID string
 | 
			
		||||
	msgID := xid.New().String()
 | 
			
		||||
	if msg.ID != "" {
 | 
			
		||||
		msgReplaceID = msg.ID
 | 
			
		||||
	}
 | 
			
		||||
	var replyID string
 | 
			
		||||
	if res, ok := b.StanzaIDs.Get(msg.ParentID); ok {
 | 
			
		||||
		replyID, _ = res.(string)
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("=> Sending message %#v", msg)
 | 
			
		||||
	if _, err := b.xc.Send(xmpp.Chat{
 | 
			
		||||
		Type:      "groupchat",
 | 
			
		||||
@@ -137,6 +153,7 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
 | 
			
		||||
		Text:      msg.Username + msg.Text,
 | 
			
		||||
		ID:        msgID,
 | 
			
		||||
		ReplaceID: msgReplaceID,
 | 
			
		||||
		ReplyID:   replyID,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
@@ -297,6 +314,11 @@ func (b *Bxmpp) handleXMPP() error {
 | 
			
		||||
			if v.Type == "groupchat" {
 | 
			
		||||
				b.Log.Debugf("== Receiving %#v", v)
 | 
			
		||||
 | 
			
		||||
				if v.ID != "" && v.StanzaID != "" {
 | 
			
		||||
					b.StanzaIDs.Add(v.ID, v.StanzaID)
 | 
			
		||||
					b.OriginIDs.Add(v.StanzaID, v.ID)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Skip invalid messages.
 | 
			
		||||
				if b.skipMessage(v) {
 | 
			
		||||
					continue
 | 
			
		||||
@@ -321,6 +343,12 @@ func (b *Bxmpp) handleXMPP() error {
 | 
			
		||||
				if v.ReplaceID != "" {
 | 
			
		||||
					msgID = v.ReplaceID
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				var parentID string
 | 
			
		||||
				if res, ok := b.OriginIDs.Get(v.ReplyID); ok {
 | 
			
		||||
					parentID, _ = res.(string)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				rmsg := config.Message{
 | 
			
		||||
					Username: b.parseNick(v.Remote),
 | 
			
		||||
					Text:     v.Text,
 | 
			
		||||
@@ -328,6 +356,7 @@ func (b *Bxmpp) handleXMPP() error {
 | 
			
		||||
					Account:  b.Account,
 | 
			
		||||
					Avatar:   avatar,
 | 
			
		||||
					UserID:   v.Remote,
 | 
			
		||||
					ParentID: parentID,
 | 
			
		||||
					ID:       msgID,
 | 
			
		||||
					Event:    event,
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -106,30 +106,31 @@ func (b *Bzulip) getChannel(id int) string {
 | 
			
		||||
func (b *Bzulip) handleQueue() error {
 | 
			
		||||
	for {
 | 
			
		||||
		messages, err := b.q.GetEvents()
 | 
			
		||||
		switch err {
 | 
			
		||||
		case gzb.BackoffError:
 | 
			
		||||
			time.Sleep(time.Second * 5)
 | 
			
		||||
		case gzb.NoJSONError:
 | 
			
		||||
			b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds")
 | 
			
		||||
			time.Sleep(time.Second * 10)
 | 
			
		||||
		case gzb.BadEventQueueError:
 | 
			
		||||
			b.Log.Info("got a bad event queue id error, reconnecting")
 | 
			
		||||
			b.bot.Queues = nil
 | 
			
		||||
			for {
 | 
			
		||||
				b.q, err = b.bot.RegisterAll()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
 | 
			
		||||
					time.Sleep(time.Second * 10)
 | 
			
		||||
				}
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		case gzb.HeartbeatError:
 | 
			
		||||
			b.Log.Debug("heartbeat received.")
 | 
			
		||||
		default:
 | 
			
		||||
			b.Log.Debugf("receiving error: %#v", err)
 | 
			
		||||
			time.Sleep(time.Second * 10)
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			switch err {
 | 
			
		||||
			case gzb.BackoffError:
 | 
			
		||||
				time.Sleep(time.Second * 5)
 | 
			
		||||
			case gzb.NoJSONError:
 | 
			
		||||
				b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds")
 | 
			
		||||
				time.Sleep(time.Second * 10)
 | 
			
		||||
			case gzb.BadEventQueueError:
 | 
			
		||||
				b.Log.Info("got a bad event queue id error, reconnecting")
 | 
			
		||||
				b.bot.Queues = nil
 | 
			
		||||
				for {
 | 
			
		||||
					b.q, err = b.bot.RegisterAll()
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
 | 
			
		||||
						time.Sleep(time.Second * 10)
 | 
			
		||||
					}
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			case gzb.HeartbeatError:
 | 
			
		||||
				b.Log.Debug("heartbeat received.")
 | 
			
		||||
			default:
 | 
			
		||||
				b.Log.Debugf("receiving error: %#v", err)
 | 
			
		||||
				time.Sleep(time.Second * 10)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		for _, m := range messages {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										165
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										165
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,3 +1,165 @@
 | 
			
		||||
# v1.26.0
 | 
			
		||||
 | 
			
		||||
## New features
 | 
			
		||||
 | 
			
		||||
- irc: Allow substitution of bot's nick in RunCommands (irc) (#1890)
 | 
			
		||||
- matrix: Add Matrix username spoofing (#1875)
 | 
			
		||||
 | 
			
		||||
## Enhancements
 | 
			
		||||
 | 
			
		||||
- general: Update dependencies (#1951)
 | 
			
		||||
- mattermost: Remove mattermost 5 support (#1936)
 | 
			
		||||
- mumble: Implement sending of EventJoinLeave both to and from Mumble (#1915)
 | 
			
		||||
- whatsappmulti: Improve attachment handling (whatsapp) (#1928)
 | 
			
		||||
- whatsappmulti: Handle incoming document captions from whatsapp (#1935)
 | 
			
		||||
 | 
			
		||||
## Bugfix
 | 
			
		||||
 | 
			
		||||
- irc: Fix empty messages on IRC (#1897)
 | 
			
		||||
- telegram: Fix message html entities escaping when sending to Telegram (#1855)
 | 
			
		||||
- telegram/slack: Fix error messages in telegram and slack bridges (#1862)
 | 
			
		||||
- telegram: Fix telegram attachment comment formatting and escaping (#1920)
 | 
			
		||||
- telegram: Make the cgo lottie a build tag (-tag cgolottie) (#1955)
 | 
			
		||||
- whatsappmulti: Update dependencies and fix whatsmeow API changes (#1887)
 | 
			
		||||
- whatsappmulti: Fix the "Someone" nickname problem (whatsapp) (#1931)
 | 
			
		||||
 | 
			
		||||
This release couldn't exist without the following contributors:
 | 
			
		||||
@s3lph, @sas1024, @Glandos, @jx11r, @Lucki, @BuckarooBanzay, @ilmaisin, @Kufat
 | 
			
		||||
 | 
			
		||||
# v1.25.2
 | 
			
		||||
 | 
			
		||||
## Enhancements
 | 
			
		||||
 | 
			
		||||
- general: Update dependencies (#1851,#1841)
 | 
			
		||||
- mattermost: Support mattermost v7.x (#1852)
 | 
			
		||||
 | 
			
		||||
## Bugfix
 | 
			
		||||
 | 
			
		||||
- discord: Fix Unwanted join notifications from one Discord server to another (#1612)
 | 
			
		||||
- discord: Ignore events from other guilds, add nosendjoinpart support (#1846)
 | 
			
		||||
 | 
			
		||||
This release couldn't exist without the following contributors:
 | 
			
		||||
@wlcx
 | 
			
		||||
 | 
			
		||||
# v1.25.1
 | 
			
		||||
 | 
			
		||||
## Enhancements
 | 
			
		||||
 | 
			
		||||
- matrix: Add KeepQuotedReply option for matrix to fix regression (#1823)
 | 
			
		||||
- slack: Improve Slack attachments formatting (slack) (#1807)
 | 
			
		||||
 | 
			
		||||
## Bugfix
 | 
			
		||||
 | 
			
		||||
- general: Update dependencies (#1813,#1822,#1833)
 | 
			
		||||
- mattermost: Add space between filename and URL (mattermost). Fixes #1820
 | 
			
		||||
- matrix: Update matterbridge/gomatrix. Fixes #1772 (#1803)
 | 
			
		||||
- telegram: Do not modify .webm files (telegram). Fixes #17**88 (#1802)
 | 
			
		||||
- telegram: Do not apply any markup to URL entities (telegram) (#1808)
 | 
			
		||||
- telegram: Fix telegram message deletion request (#1818)
 | 
			
		||||
- vk: Fix UploadMessagesPhoto for vk community chat (vk) (#1812)
 | 
			
		||||
 | 
			
		||||
This release couldn't exist without the following contributors:
 | 
			
		||||
@bd808, @chugunov, @sas1024, @SevereCloud, @ValdikSS
 | 
			
		||||
 | 
			
		||||
# v1.25.0
 | 
			
		||||
 | 
			
		||||
## Breaking changes
 | 
			
		||||
 | 
			
		||||
- whatsapp: deprecated, the library <https://github.com/Rhymen/go-whatsapp> isn't maintained anymore.
 | 
			
		||||
We're switching to <https://github.com/tulir/whatsmeow> but as this uses a GPL3 licensed library we can't provide you with binaries.
 | 
			
		||||
You'll need to build it yourself. More information about this can be found here: <https://github.com/42wim/matterbridge#building-with-whatsapp-beta-multidevice-support>
 | 
			
		||||
 | 
			
		||||
## New features
 | 
			
		||||
 | 
			
		||||
- whatsappmulti: whatsapp multidevice support added - more info <https://github.com/42wim/matterbridge#building-with-whatsapp-beta-multidevice-support>
 | 
			
		||||
- general: Add Dockerfile_whatsappmulti for building with WhatsApp Multi-Device support (Whatsmeow) (#1774)
 | 
			
		||||
- telegram: Add UseFullName option (telegram) (#1777)
 | 
			
		||||
- slack: Use slack real name as user name (slack) (#1775)
 | 
			
		||||
 | 
			
		||||
## Enhancements
 | 
			
		||||
 | 
			
		||||
- general: Ignore sending file with comment, if comment contains IgnoreMessages value (#1783)
 | 
			
		||||
- general: Update dependencies (#1784)
 | 
			
		||||
- irc: Update lrstanley/girc dep (#1773)
 | 
			
		||||
- slack: Preserve threading for messages with files (slack) (#1781)
 | 
			
		||||
- telegram: Preserve threading from telegram replies (telegram) (#1776)
 | 
			
		||||
- telegram: Multiple media in one message (telegram) (#1779)
 | 
			
		||||
- whatsapp: Add whatsapp deprecation warning (#1792)
 | 
			
		||||
 | 
			
		||||
## Bugfix
 | 
			
		||||
 | 
			
		||||
- discord: Change discord non-native threading behaviour (discord) (#1791)
 | 
			
		||||
 | 
			
		||||
This release couldn't exist without the following contributors:
 | 
			
		||||
@sas1024, @tpxtron
 | 
			
		||||
 | 
			
		||||
# v1.24.1
 | 
			
		||||
 | 
			
		||||
## Enhancements
 | 
			
		||||
 | 
			
		||||
- discord: Switch to discordgo upstream again (#1759)
 | 
			
		||||
- general: Update dependencies and vendor (#1761)
 | 
			
		||||
- general: Create inmessage-logger.tengo (#1688) (#1747)
 | 
			
		||||
- general: Add OpenRC service file (#1746)
 | 
			
		||||
- irc: Refactor utf-8 conversion (irc) (#1767)
 | 
			
		||||
 | 
			
		||||
## Bugfixes
 | 
			
		||||
 | 
			
		||||
- irc: Fix panic in irc. Closes #1751 (#1760)
 | 
			
		||||
- mumble: Implement a workaround to signal Opus support (mumble) (#1764)
 | 
			
		||||
- telegram: Fix for complex-formatted Telegram text (#1765)
 | 
			
		||||
- telegram: Fix Telegram channel title in forwards (#1753)
 | 
			
		||||
- telegram: Fix Telegram Problem (unforwarded formatting and skipping of linebreaks) (#1749)
 | 
			
		||||
 | 
			
		||||
This release couldn't exist without the following contributors:
 | 
			
		||||
@s3lph, @ValdikSS, @reckel-jm, @CyberTailor
 | 
			
		||||
 | 
			
		||||
# v1.24.0
 | 
			
		||||
 | 
			
		||||
## New features
 | 
			
		||||
 | 
			
		||||
- harmony: new protocol added: Add support for Harmony (#1656)
 | 
			
		||||
- irc: Allow binding to IP on IRC (#1640)
 | 
			
		||||
- irc: Add support for client certificate (irc) (#1710)
 | 
			
		||||
- mattermost: Add UseUsername option (mattermost). Fixes #1665 (#1714)
 | 
			
		||||
- mattermost: Add support for using ID in channel config (mattermost) (#1715)
 | 
			
		||||
- matrix: Reply support for Matrix (#1664)
 | 
			
		||||
- telegram: Add Telegram Bot Command /chatId (telegram) (#1703)
 | 
			
		||||
 | 
			
		||||
## Enhancements
 | 
			
		||||
 | 
			
		||||
- general: Update dependencies/vendor (#1659)
 | 
			
		||||
- discord: Add more debug options for discord (#1712)
 | 
			
		||||
- docker: Use Alpine stable again in Dockerfile (#1643)
 | 
			
		||||
- mattermost: Log eventtype in debug (mattermost) (#1676)
 | 
			
		||||
- mattermost: Add more ignore debug messages (mattermost) (#1678)
 | 
			
		||||
- slack: Add support for deleting files from slack to discord. Fixes #1705 (#1709)
 | 
			
		||||
- telegram: Add support for code blocks in telegram (#1650)
 | 
			
		||||
- telegram: Update telegram-bot-api to v5 (#1660)
 | 
			
		||||
- telegram: Add comments to messages (telegram) (#1652)
 | 
			
		||||
- telegram: Add support for sender_chat (telegram) (#1677)
 | 
			
		||||
- vk: Remove GroupID (vk) (#1668)
 | 
			
		||||
 | 
			
		||||
## Bugfix
 | 
			
		||||
 | 
			
		||||
- mattermost: Use current parentID if rootId is not set (mattermost) (#1675)
 | 
			
		||||
- matrix: Make HTMLDisable work correct (matrix) (#1716)
 | 
			
		||||
- whatsapp: Make EditSuffix option actually work (whatsapp). Fixes #1510 (#1728)
 | 
			
		||||
 | 
			
		||||
This release couldn't exist without the following contributors:
 | 
			
		||||
@DavyJohnesev, @GoliathLabs, @pontaoski, @PeGaSuS-Coder, @dependabot[bot], @vpzomtrrfrt, @SevereCloud, @soloam, @YashRE42, @danwalmsley, @SuperSandro2000, @inzanity
 | 
			
		||||
 | 
			
		||||
# v1.23.2
 | 
			
		||||
 | 
			
		||||
If you're running whatsapp you should update.
 | 
			
		||||
 | 
			
		||||
## Bugfix
 | 
			
		||||
 | 
			
		||||
- whatsapp: Update go-whatsapp version (#1630)
 | 
			
		||||
 | 
			
		||||
This release couldn't exist without the following contributors:
 | 
			
		||||
@snikpic
 | 
			
		||||
 | 
			
		||||
# v1.23.1
 | 
			
		||||
 | 
			
		||||
If you're running mattermost 6 you should update.
 | 
			
		||||
@@ -12,6 +174,9 @@ If you're running mattermost 6 you should update.
 | 
			
		||||
- xmpp: Use a new msgID when replacing messages (xmpp). Fixes #1584 (#1623)
 | 
			
		||||
- zulip: Add better error handling on Zulip (#1589)
 | 
			
		||||
 | 
			
		||||
This release couldn't exist without the following contributors:
 | 
			
		||||
@Polynomdivision, @minecraftchest1, @alexmv
 | 
			
		||||
 | 
			
		||||
# v1.23.0
 | 
			
		||||
 | 
			
		||||
## New features
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								contrib/inmessage-logger.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								contrib/inmessage-logger.tengo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
fmt := import("fmt")
 | 
			
		||||
os := import("os")
 | 
			
		||||
times := import("times")
 | 
			
		||||
 | 
			
		||||
if msgText != "" && msgUsername != "system" {
 | 
			
		||||
    os.chdir("/var/www/matterbridge")
 | 
			
		||||
    file := os.open_file("inmessage.log", os.o_append|os.o_wronly|os.o_create, 0644)
 | 
			
		||||
    file.write_string(fmt.sprintf(
 | 
			
		||||
        "[%s] <%s> %s\n",
 | 
			
		||||
        times.time_format(times.now(), times.format_rfc1123),
 | 
			
		||||
        msgUsername,
 | 
			
		||||
        msgText
 | 
			
		||||
    ))
 | 
			
		||||
    file.close()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								contrib/matterbridge.openrc
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										19
									
								
								contrib/matterbridge.openrc
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
#!/sbin/openrc-run
 | 
			
		||||
# Copyright 2021-2022 Gentoo Authors
 | 
			
		||||
# Distributed under the terms of the GNU General Public License v2
 | 
			
		||||
 | 
			
		||||
command=/usr/bin/matterbridge
 | 
			
		||||
command_args="-conf ${MATTERBRIDGE_CONF:-/etc/matterbridge/bridge.toml} ${MATTERBRIDGE_ARGS}"
 | 
			
		||||
command_user="matterbridge:matterbridge"
 | 
			
		||||
pidfile="/run/${RC_SVCNAME}.pid"
 | 
			
		||||
command_background=1
 | 
			
		||||
output_log="/var/log/${RC_SVCNAME}.log"
 | 
			
		||||
error_log="${output_log}"
 | 
			
		||||
 | 
			
		||||
depend() {
 | 
			
		||||
	need net
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
start_pre() {
 | 
			
		||||
	checkpath -f "${output_log}" -o "${command_user}" || return 1
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								contrib/mumble-autolink.tengo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								contrib/mumble-autolink.tengo
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
text := import("text")
 | 
			
		||||
 | 
			
		||||
if outProtocol == "mumble" {
 | 
			
		||||
    urlRE := text.re_compile(`(?is)((http|https):\/\/)?([a-z0-9-]+\.)?[a-z0-9-]+(\.[a-z]{2,6}){1,3}(\/[a-z0-9.,_\/~#&=;%+?-]*)?`)
 | 
			
		||||
    msgText = urlRE.replace(msgText,`<a href="$0">$0</a>`)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !nogitter
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bgitter "github.com/42wim/matterbridge/bridge/gitter"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["gitter"] = bgitter.New
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								gateway/bridgemap/bharmony.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								gateway/bridgemap/bharmony.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
//go:build !noharmony
 | 
			
		||||
// +build !noharmony
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bharmony "github.com/42wim/matterbridge/bridge/harmony"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["harmony"] = bharmony.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
// +build !nowhatsapp
 | 
			
		||||
// +build !whatsappmulti
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								gateway/bridgemap/bwhatsappmulti.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gateway/bridgemap/bwhatsappmulti.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
// +build whatsappmulti
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bwhatsapp "github.com/42wim/matterbridge/bridge/whatsappmulti"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["whatsapp"] = bwhatsapp.New
 | 
			
		||||
}
 | 
			
		||||
@@ -66,7 +66,7 @@ func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway {
 | 
			
		||||
func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
 | 
			
		||||
	ID := protocol + " " + mID
 | 
			
		||||
	if gw.Messages.Contains(ID) {
 | 
			
		||||
		return mID
 | 
			
		||||
		return ID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If not keyed, iterate through cache for downstream, and infer upstream.
 | 
			
		||||
@@ -75,7 +75,7 @@ func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
 | 
			
		||||
		ids := v.([]*BrMsgID)
 | 
			
		||||
		for _, downstreamMsgObj := range ids {
 | 
			
		||||
			if ID == downstreamMsgObj.ID {
 | 
			
		||||
				return strings.Replace(mid.(string), protocol+" ", "", 1)
 | 
			
		||||
				return mid.(string)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -299,13 +299,30 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
 | 
			
		||||
 | 
			
		||||
	igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks"))
 | 
			
		||||
	igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages"))
 | 
			
		||||
	if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) {
 | 
			
		||||
	if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) || gw.ignoreFilesComment(msg.Extra, igMessages) {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ignoreFilesComment returns true if we need to ignore a file with matched comment.
 | 
			
		||||
func (gw *Gateway) ignoreFilesComment(extra map[string][]interface{}, igMessages []string) bool {
 | 
			
		||||
	if extra == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	for _, f := range extra["file"] {
 | 
			
		||||
		fi, ok := f.(config.FileInfo)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if gw.ignoreText(fi.Comment, igMessages) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string {
 | 
			
		||||
	if dest.GetBool("StripNick") {
 | 
			
		||||
		re := regexp.MustCompile("[^a-zA-Z0-9]+")
 | 
			
		||||
@@ -447,16 +464,19 @@ func (gw *Gateway) SendMessage(
 | 
			
		||||
	msg.Avatar = gw.modifyAvatar(rmsg, dest)
 | 
			
		||||
	msg.Username = gw.modifyUsername(rmsg, dest)
 | 
			
		||||
 | 
			
		||||
	msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel)
 | 
			
		||||
	// exclude file delete event as the msg ID here is the native file ID that needs to be deleted
 | 
			
		||||
	if msg.Event != config.EventFileDelete {
 | 
			
		||||
		msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// for api we need originchannel as channel
 | 
			
		||||
	if dest.Protocol == apiProtocol {
 | 
			
		||||
		msg.Channel = rmsg.Channel
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel)
 | 
			
		||||
	msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel)
 | 
			
		||||
	if msg.ParentID == "" {
 | 
			
		||||
		msg.ParentID = canonicalParentMsgID
 | 
			
		||||
		msg.ParentID = strings.Replace(canonicalParentMsgID, dest.Protocol+" ", "", 1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if the parentID is still empty and we have a parentID set in the original message
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,6 @@ var testconfig = []byte(`
 | 
			
		||||
server=""
 | 
			
		||||
[mattermost.test]
 | 
			
		||||
server=""
 | 
			
		||||
[gitter.42wim]
 | 
			
		||||
server=""
 | 
			
		||||
[discord.test]
 | 
			
		||||
server=""
 | 
			
		||||
[slack.test]
 | 
			
		||||
@@ -33,11 +31,6 @@ server=""
 | 
			
		||||
    account = "irc.freenode"
 | 
			
		||||
    channel = "#wimtesting"
 | 
			
		||||
    
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
    account="gitter.42wim"
 | 
			
		||||
    channel="42wim/testroom"
 | 
			
		||||
    #channel="matterbridge/Lobby"
 | 
			
		||||
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
    account = "discord.test"
 | 
			
		||||
    channel = "general"
 | 
			
		||||
@@ -52,8 +45,6 @@ var testconfig2 = []byte(`
 | 
			
		||||
server=""
 | 
			
		||||
[mattermost.test]
 | 
			
		||||
server=""
 | 
			
		||||
[gitter.42wim]
 | 
			
		||||
server=""
 | 
			
		||||
[discord.test]
 | 
			
		||||
server=""
 | 
			
		||||
[slack.test]
 | 
			
		||||
@@ -67,10 +58,6 @@ server=""
 | 
			
		||||
    account = "irc.freenode"
 | 
			
		||||
    channel = "#wimtesting"
 | 
			
		||||
    
 | 
			
		||||
    [[gateway.in]]
 | 
			
		||||
    account="gitter.42wim"
 | 
			
		||||
    channel="42wim/testroom"
 | 
			
		||||
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
    account = "discord.test"
 | 
			
		||||
    channel = "general"
 | 
			
		||||
@@ -86,10 +73,6 @@ server=""
 | 
			
		||||
    account = "irc.freenode"
 | 
			
		||||
    channel = "#wimtesting2"
 | 
			
		||||
    
 | 
			
		||||
    [[gateway.out]]
 | 
			
		||||
    account="gitter.42wim"
 | 
			
		||||
    channel="42wim/testroom"
 | 
			
		||||
 | 
			
		||||
    [[gateway.out]]
 | 
			
		||||
    account = "discord.test"
 | 
			
		||||
    channel = "general2"
 | 
			
		||||
@@ -184,31 +167,18 @@ func maketestRouter(input []byte) *Router {
 | 
			
		||||
	}
 | 
			
		||||
	return r
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewRouter(t *testing.T) {
 | 
			
		||||
	r := maketestRouter(testconfig)
 | 
			
		||||
	assert.Equal(t, 1, len(r.Gateways))
 | 
			
		||||
	assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
 | 
			
		||||
	assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
 | 
			
		||||
	assert.Equal(t, 3, len(r.Gateways["bridge1"].Bridges))
 | 
			
		||||
	assert.Equal(t, 3, len(r.Gateways["bridge1"].Channels))
 | 
			
		||||
	r = maketestRouter(testconfig2)
 | 
			
		||||
	assert.Equal(t, 2, len(r.Gateways))
 | 
			
		||||
	assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
 | 
			
		||||
	assert.Equal(t, 3, len(r.Gateways["bridge2"].Bridges))
 | 
			
		||||
	assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
 | 
			
		||||
	assert.Equal(t, 3, len(r.Gateways["bridge2"].Channels))
 | 
			
		||||
	assert.Equal(t, &config.ChannelInfo{
 | 
			
		||||
		Name:        "42wim/testroom",
 | 
			
		||||
		Direction:   "out",
 | 
			
		||||
		ID:          "42wim/testroomgitter.42wim",
 | 
			
		||||
		Account:     "gitter.42wim",
 | 
			
		||||
		SameChannel: map[string]bool{"bridge2": false},
 | 
			
		||||
	}, r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"])
 | 
			
		||||
	assert.Equal(t, &config.ChannelInfo{
 | 
			
		||||
		Name:        "42wim/testroom",
 | 
			
		||||
		Direction:   "in",
 | 
			
		||||
		ID:          "42wim/testroomgitter.42wim",
 | 
			
		||||
		Account:     "gitter.42wim",
 | 
			
		||||
		SameChannel: map[string]bool{"bridge1": false},
 | 
			
		||||
	}, r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"])
 | 
			
		||||
	assert.Equal(t, 3, len(r.Gateways["bridge1"].Bridges))
 | 
			
		||||
	assert.Equal(t, 2, len(r.Gateways["bridge2"].Bridges))
 | 
			
		||||
	assert.Equal(t, 3, len(r.Gateways["bridge1"].Channels))
 | 
			
		||||
	assert.Equal(t, 2, len(r.Gateways["bridge2"].Channels))
 | 
			
		||||
	assert.Equal(t, &config.ChannelInfo{
 | 
			
		||||
		Name:        "general",
 | 
			
		||||
		Direction:   "inout",
 | 
			
		||||
@@ -241,8 +211,6 @@ func TestGetDestChannel(t *testing.T) {
 | 
			
		||||
				SameChannel: map[string]bool{"bridge1": false},
 | 
			
		||||
				Options:     config.ChannelOptions{Key: ""},
 | 
			
		||||
			}}, r.Gateways["bridge1"].getDestChannel(msg, *br))
 | 
			
		||||
		case "gitter.42wim":
 | 
			
		||||
			assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
 | 
			
		||||
		case "irc.freenode":
 | 
			
		||||
			assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
 | 
			
		||||
		}
 | 
			
		||||
@@ -420,6 +388,7 @@ func (s *ignoreTestSuite) SetupSuite() {
 | 
			
		||||
	logger.SetOutput(ioutil.Discard)
 | 
			
		||||
	s.gw = &Gateway{logger: logrus.NewEntry(logger)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *ignoreTestSuite) TestIgnoreTextEmpty() {
 | 
			
		||||
	extraFile := make(map[string][]interface{})
 | 
			
		||||
	extraAttach := make(map[string][]interface{})
 | 
			
		||||
@@ -461,7 +430,6 @@ func (s *ignoreTestSuite) TestIgnoreTextEmpty() {
 | 
			
		||||
		output := s.gw.ignoreTextEmpty(testcase.input)
 | 
			
		||||
		s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *ignoreTestSuite) TestIgnoreTexts() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										198
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										198
									
								
								go.mod
									
									
									
									
									
								
							@@ -1,136 +1,154 @@
 | 
			
		||||
module github.com/42wim/matterbridge
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
 | 
			
		||||
	github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
 | 
			
		||||
	github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f
 | 
			
		||||
	github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560
 | 
			
		||||
	github.com/Rhymen/go-whatsapp v0.1.2-0.20210615184944-2b8a3e9b8aa2
 | 
			
		||||
	github.com/SevereCloud/vksdk/v2 v2.10.0
 | 
			
		||||
	github.com/d5/tengo/v2 v2.8.0
 | 
			
		||||
	github.com/davecgh/go-spew v1.1.1
 | 
			
		||||
	github.com/fsnotify/fsnotify v1.5.1
 | 
			
		||||
	github.com/go-telegram-bot-api/telegram-bot-api v1.0.1-0.20200524105306-7434b0456e81
 | 
			
		||||
	github.com/gomarkdown/markdown v0.0.0-20210918233619-6c1113f12c4a
 | 
			
		||||
	github.com/google/gops v0.3.22
 | 
			
		||||
	github.com/gorilla/schema v1.2.0
 | 
			
		||||
	github.com/gorilla/websocket v1.4.2
 | 
			
		||||
	github.com/hashicorp/golang-lru v0.5.4
 | 
			
		||||
	github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c
 | 
			
		||||
	github.com/SevereCloud/vksdk/v2 v2.17.0
 | 
			
		||||
	github.com/bwmarrin/discordgo v0.29.0
 | 
			
		||||
	github.com/d5/tengo/v2 v2.17.0
 | 
			
		||||
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 | 
			
		||||
	github.com/fsnotify/fsnotify v1.7.0
 | 
			
		||||
	github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2
 | 
			
		||||
	github.com/google/gops v0.3.27
 | 
			
		||||
	github.com/gorilla/schema v1.4.1
 | 
			
		||||
	github.com/harmony-development/shibshib v0.0.0-20220101224523-c98059d09cfa
 | 
			
		||||
	github.com/hashicorp/golang-lru v1.0.2
 | 
			
		||||
	github.com/jpillora/backoff v1.0.0
 | 
			
		||||
	github.com/keybase/go-keybase-chat-bot v0.0.0-20211004153716-fd2ee4d6be11
 | 
			
		||||
	github.com/kyokomi/emoji/v2 v2.2.8
 | 
			
		||||
	github.com/labstack/echo/v4 v4.6.1
 | 
			
		||||
	github.com/lrstanley/girc v0.0.0-20210611213246-771323f1624b
 | 
			
		||||
	github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
 | 
			
		||||
	github.com/keybase/go-keybase-chat-bot v0.0.0-20221220212439-e48d9abd2c20
 | 
			
		||||
	github.com/kyokomi/emoji/v2 v2.2.13
 | 
			
		||||
	github.com/labstack/echo/v4 v4.12.0
 | 
			
		||||
	github.com/lrstanley/girc v0.0.0-20240823210506-80555f2adb03
 | 
			
		||||
	github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696
 | 
			
		||||
	github.com/matterbridge/discordgo v0.21.2-0.20210201201054-fb39a175b4f7
 | 
			
		||||
	github.com/matterbridge/go-xmpp v0.0.0-20210731150933-5702291c239f
 | 
			
		||||
	github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8
 | 
			
		||||
	github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27
 | 
			
		||||
	github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75
 | 
			
		||||
	github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba
 | 
			
		||||
	github.com/matterbridge/matterclient v0.0.0-20211024214211-22e762684b4a
 | 
			
		||||
	github.com/mattermost/mattermost-server/v5 v5.39.0
 | 
			
		||||
	github.com/mattermost/mattermost-server/v6 v6.0.2
 | 
			
		||||
	github.com/matterbridge/matterclient v0.0.0-20240817214420-3d4c3aef3dc1
 | 
			
		||||
	github.com/matterbridge/telegram-bot-api/v6 v6.5.0
 | 
			
		||||
	github.com/mattermost/mattermost/server/public v0.1.6
 | 
			
		||||
	github.com/mattn/godown v0.0.1
 | 
			
		||||
	github.com/missdeer/golib v1.0.4
 | 
			
		||||
	github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9
 | 
			
		||||
	github.com/mdp/qrterminal v1.0.1
 | 
			
		||||
	github.com/mitchellh/mapstructure v1.5.0
 | 
			
		||||
	github.com/nelsonken/gomf v0.0.0-20190423072027-c65cc0469e94
 | 
			
		||||
	github.com/olahol/melody v1.2.1
 | 
			
		||||
	github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c
 | 
			
		||||
	github.com/rs/xid v1.3.0
 | 
			
		||||
	github.com/rs/xid v1.5.0
 | 
			
		||||
	github.com/russross/blackfriday v1.6.0
 | 
			
		||||
	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca
 | 
			
		||||
	github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
 | 
			
		||||
	github.com/shazow/ssh-chat v1.10.1
 | 
			
		||||
	github.com/sirupsen/logrus v1.8.1
 | 
			
		||||
	github.com/slack-go/slack v0.9.5
 | 
			
		||||
	github.com/spf13/viper v1.9.0
 | 
			
		||||
	github.com/stretchr/testify v1.7.0
 | 
			
		||||
	github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50
 | 
			
		||||
	github.com/sirupsen/logrus v1.9.3
 | 
			
		||||
	github.com/slack-go/slack v0.14.0
 | 
			
		||||
	github.com/spf13/viper v1.19.0
 | 
			
		||||
	github.com/stretchr/testify v1.9.0
 | 
			
		||||
	github.com/vincent-petithory/dataurl v1.0.0
 | 
			
		||||
	github.com/writeas/go-strip-markdown v2.0.1+incompatible
 | 
			
		||||
	github.com/yaegashi/msgraph.go v0.1.4
 | 
			
		||||
	github.com/zfjagann/golang-ring v0.0.0-20210116075443-7c86fdb43134
 | 
			
		||||
	golang.org/x/image v0.0.0-20211028202545-6944b10bf410
 | 
			
		||||
	golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5
 | 
			
		||||
	github.com/zfjagann/golang-ring v0.0.0-20220330170733-19bcea1b6289
 | 
			
		||||
	go.mau.fi/whatsmeow v0.0.0-20240821142752-3d63c6fcc1a7
 | 
			
		||||
	golang.org/x/image v0.19.0
 | 
			
		||||
	golang.org/x/oauth2 v0.22.0
 | 
			
		||||
	golang.org/x/text v0.17.0
 | 
			
		||||
	gomod.garykim.dev/nc-talk v0.3.0
 | 
			
		||||
	gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376
 | 
			
		||||
	layeh.com/gumble v0.0.0-20200818122324-146f9205029b
 | 
			
		||||
	google.golang.org/protobuf v1.34.2
 | 
			
		||||
	layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14
 | 
			
		||||
	modernc.org/sqlite v1.32.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	filippo.io/edwards25519 v1.1.0 // indirect
 | 
			
		||||
	github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989 // indirect
 | 
			
		||||
	github.com/Jeffail/gabs v1.4.0 // indirect
 | 
			
		||||
	github.com/apex/log v1.9.0 // indirect
 | 
			
		||||
	github.com/av-elier/go-decimal-to-rational v0.0.0-20191127152832-89e6aad02ecf // indirect
 | 
			
		||||
	github.com/blang/semver 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/blang/semver/v4 v4.0.0 // indirect
 | 
			
		||||
	github.com/dustin/go-humanize v1.0.1 // indirect
 | 
			
		||||
	github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
 | 
			
		||||
	github.com/fatih/color v1.17.0 // indirect
 | 
			
		||||
	github.com/francoispqt/gojay v1.2.13 // indirect
 | 
			
		||||
	github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect
 | 
			
		||||
	github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
 | 
			
		||||
	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
 | 
			
		||||
	github.com/golang/protobuf v1.5.2 // indirect
 | 
			
		||||
	github.com/google/uuid v1.2.0 // indirect
 | 
			
		||||
	github.com/golang/protobuf v1.5.4 // indirect
 | 
			
		||||
	github.com/google/uuid v1.6.0 // indirect
 | 
			
		||||
	github.com/gopackage/ddp v0.0.3 // indirect
 | 
			
		||||
	github.com/gorilla/websocket v1.5.3 // indirect
 | 
			
		||||
	github.com/hashicorp/errwrap v1.1.0 // indirect
 | 
			
		||||
	github.com/hashicorp/go-hclog v1.6.3 // indirect
 | 
			
		||||
	github.com/hashicorp/go-multierror v1.1.1 // indirect
 | 
			
		||||
	github.com/hashicorp/go-plugin v1.6.1 // indirect
 | 
			
		||||
	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 | 
			
		||||
	github.com/hashicorp/hcl v1.0.0 // indirect
 | 
			
		||||
	github.com/json-iterator/go v1.1.11 // indirect
 | 
			
		||||
	github.com/hashicorp/yamux v0.1.1 // indirect
 | 
			
		||||
	github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect
 | 
			
		||||
	github.com/klauspost/cpuid/v2 v2.0.6 // indirect
 | 
			
		||||
	github.com/labstack/gommon v0.3.0 // indirect
 | 
			
		||||
	github.com/magiconair/properties v1.8.5 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.17.9 // indirect
 | 
			
		||||
	github.com/labstack/gommon v0.4.2 // indirect
 | 
			
		||||
	github.com/magiconair/properties v1.8.7 // indirect
 | 
			
		||||
	github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
 | 
			
		||||
	github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect
 | 
			
		||||
	github.com/mattermost/logr v1.0.13 // indirect
 | 
			
		||||
	github.com/mattermost/logr/v2 v2.0.10 // indirect
 | 
			
		||||
	github.com/mattn/go-colorable v0.1.8 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.14 // indirect
 | 
			
		||||
	github.com/mattn/go-runewidth v0.0.13 // indirect
 | 
			
		||||
	github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
 | 
			
		||||
	github.com/mattermost/logr/v2 v2.0.21 // indirect
 | 
			
		||||
	github.com/mattn/go-colorable v0.1.13 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.20 // indirect
 | 
			
		||||
	github.com/mattn/go-runewidth v0.0.15 // indirect
 | 
			
		||||
	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
 | 
			
		||||
	github.com/minio/md5-simd v1.1.2 // indirect
 | 
			
		||||
	github.com/minio/minio-go/v7 v7.0.11 // 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.2 // indirect
 | 
			
		||||
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 | 
			
		||||
	github.com/modern-go/reflect2 v1.0.1 // indirect
 | 
			
		||||
	github.com/mitchellh/go-testing-interface v1.14.1 // 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/ncruces/go-strftime v0.1.9 // indirect
 | 
			
		||||
	github.com/nxadm/tail v1.4.11 // indirect
 | 
			
		||||
	github.com/oklog/run v1.1.0 // indirect
 | 
			
		||||
	github.com/pborman/uuid v1.2.1 // indirect
 | 
			
		||||
	github.com/pelletier/go-toml v1.9.4 // indirect
 | 
			
		||||
	github.com/philhofer/fwd v1.1.1 // indirect
 | 
			
		||||
	github.com/pelletier/go-toml v1.9.5 // indirect
 | 
			
		||||
	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
 | 
			
		||||
	github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect
 | 
			
		||||
	github.com/pkg/errors v0.9.1 // indirect
 | 
			
		||||
	github.com/pmezard/go-difflib v1.0.0 // indirect
 | 
			
		||||
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 | 
			
		||||
	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 | 
			
		||||
	github.com/rickb777/date v1.12.4 // indirect
 | 
			
		||||
	github.com/rickb777/plural v1.2.0 // indirect
 | 
			
		||||
	github.com/rivo/uniseg v0.2.0 // indirect
 | 
			
		||||
	github.com/rivo/uniseg v0.4.7 // indirect
 | 
			
		||||
	github.com/rs/zerolog v1.33.0 // indirect
 | 
			
		||||
	github.com/sagikazarmark/locafero v0.4.0 // indirect
 | 
			
		||||
	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
 | 
			
		||||
	github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 // indirect
 | 
			
		||||
	github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 // indirect
 | 
			
		||||
	github.com/skip2/go-qrcode v0.0.0-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/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 // indirect
 | 
			
		||||
	github.com/sourcegraph/conc v0.3.0 // indirect
 | 
			
		||||
	github.com/spf13/afero v1.11.0 // indirect
 | 
			
		||||
	github.com/spf13/cast v1.6.0 // indirect
 | 
			
		||||
	github.com/spf13/pflag v1.0.5 // indirect
 | 
			
		||||
	github.com/subosito/gotenv v1.2.0 // indirect
 | 
			
		||||
	github.com/technoweenie/multipartstreamer v1.0.1 // indirect
 | 
			
		||||
	github.com/tinylib/msgp v1.1.6 // indirect
 | 
			
		||||
	github.com/subosito/gotenv v1.6.0 // indirect
 | 
			
		||||
	github.com/tinylib/msgp v1.2.0 // indirect
 | 
			
		||||
	github.com/valyala/bytebufferpool v1.0.0 // indirect
 | 
			
		||||
	github.com/valyala/fasttemplate v1.2.1 // indirect
 | 
			
		||||
	github.com/wiggin77/cfg v1.0.2 // indirect
 | 
			
		||||
	github.com/wiggin77/merror v1.0.3 // indirect
 | 
			
		||||
	github.com/valyala/fasttemplate v1.2.2 // indirect
 | 
			
		||||
	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
 | 
			
		||||
	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 | 
			
		||||
	github.com/wiggin77/merror v1.0.5 // indirect
 | 
			
		||||
	github.com/wiggin77/srslog v1.0.1 // indirect
 | 
			
		||||
	go.uber.org/atomic v1.8.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-20210817164053-32db794688a5 // indirect
 | 
			
		||||
	golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect
 | 
			
		||||
	golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // 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.63.2 // indirect
 | 
			
		||||
	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
 | 
			
		||||
	go.mau.fi/libsignal v0.1.1 // indirect
 | 
			
		||||
	go.mau.fi/util v0.6.0 // indirect
 | 
			
		||||
	go.uber.org/multierr v1.11.0 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.25.0 // indirect
 | 
			
		||||
	golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
 | 
			
		||||
	golang.org/x/net v0.27.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.22.0 // indirect
 | 
			
		||||
	golang.org/x/term v0.22.0 // indirect
 | 
			
		||||
	golang.org/x/time v0.5.0 // indirect
 | 
			
		||||
	google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect
 | 
			
		||||
	google.golang.org/grpc v1.65.0 // indirect
 | 
			
		||||
	gopkg.in/ini.v1 v1.67.0 // indirect
 | 
			
		||||
	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 | 
			
		||||
	gopkg.in/yaml.v2 v2.4.0 // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
			
		||||
	modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
 | 
			
		||||
	modernc.org/libc v1.55.3 // indirect
 | 
			
		||||
	modernc.org/mathutil v1.6.0 // indirect
 | 
			
		||||
	modernc.org/memory v1.8.0 // indirect
 | 
			
		||||
	modernc.org/strutil v1.2.0 // indirect
 | 
			
		||||
	modernc.org/token v1.1.0 // indirect
 | 
			
		||||
	rsc.io/qr v0.2.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
go 1.17
 | 
			
		||||
//replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419
 | 
			
		||||
replace github.com/matterbridge/go-xmpp => "/home/irc-discord/uwaru-go-xmpp"
 | 
			
		||||
 | 
			
		||||
go 1.22.0
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"regexp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Message for rocketchat outgoing webhook.
 | 
			
		||||
@@ -68,7 +69,6 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	}
 | 
			
		||||
	msg := Message{}
 | 
			
		||||
	body, err := ioutil.ReadAll(r.Body)
 | 
			
		||||
	log.Println(string(body))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
		http.NotFound(w, r)
 | 
			
		||||
@@ -89,7 +89,11 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	msg.ChannelName = "#" + msg.ChannelName
 | 
			
		||||
	if c.Token != "" {
 | 
			
		||||
		if msg.Token != c.Token {
 | 
			
		||||
			log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr)
 | 
			
		||||
			if regexp.MustCompile(`[^a-zA-Z0-9]+`).MatchString(msg.Token) {
 | 
			
		||||
				log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr)
 | 
			
		||||
			} else {
 | 
			
		||||
				log.Println("invalid token from " + r.RemoteAddr)
 | 
			
		||||
			}
 | 
			
		||||
			http.NotFound(w, r)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ func main() {
 | 
			
		||||
	if err = r.Start(); err != nil {
 | 
			
		||||
		logger.Fatalf("Starting gateway failed: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	logger.Printf("Gateway(s) started succesfully. Now relaying messages")
 | 
			
		||||
	logger.Printf("Gateway(s) started successfully. Now relaying messages")
 | 
			
		||||
	select {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								matterbridge.toml.multi
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								matterbridge.toml.multi
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
#WARNING: as this file contains credentials, be sure to set correct file permissions
 | 
			
		||||
 | 
			
		||||
[irc]
 | 
			
		||||
    [irc.foo]
 | 
			
		||||
    Server="irc.myfooserver.com:6667"
 | 
			
		||||
    Nick="matterbot"
 | 
			
		||||
 | 
			
		||||
# Can also connect to multiple different servers of the same protocol:
 | 
			
		||||
[irc]
 | 
			
		||||
    [irc.bar]
 | 
			
		||||
    Server="irc.mybarserver.com:6667"
 | 
			
		||||
    Nick="matterbot"
 | 
			
		||||
 | 
			
		||||
[telegram]
 | 
			
		||||
    [telegram.mytelegram]
 | 
			
		||||
    Token="123456789:FillInYourTokenHereThatIsImportant"
 | 
			
		||||
 | 
			
		||||
[mattermost]
 | 
			
		||||
    [mattermost.work]
 | 
			
		||||
    #do not prefix it wit http:// or https://
 | 
			
		||||
    Server="yourmattermostserver.domain" 
 | 
			
		||||
    Team="yourteam"
 | 
			
		||||
    Login="yourlogin"
 | 
			
		||||
    Password="yourpass"
 | 
			
		||||
    PrefixMessagesWithNick=true
 | 
			
		||||
 | 
			
		||||
# Bridge 1: Copy all messages from all rooms to all rooms.
 | 
			
		||||
# This shows how you can have multiple rooms in a single bridge.
 | 
			
		||||
[[gateway]]
 | 
			
		||||
name="cats-are-cool"
 | 
			
		||||
enable=true
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
    account="irc.foo"
 | 
			
		||||
    channel="#cats-are-cool"
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
    account="irc.bar"
 | 
			
		||||
    channel="#cats-are-cool"
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
    account="telegram.mytelegram"
 | 
			
		||||
    channel="-1234567890123"
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
    account="mattermost.work"
 | 
			
		||||
    channel="cats-are-cool"
 | 
			
		||||
 | 
			
		||||
# Bridge 2: Copy some messages from some rooms to some rooms.
 | 
			
		||||
# This shows how you can have multiple bridges.
 | 
			
		||||
[[gateway]]
 | 
			
		||||
name="dog-announcements"
 | 
			
		||||
enable=true
 | 
			
		||||
    [[gateway.in]]
 | 
			
		||||
    account="irc.foo"
 | 
			
		||||
    channel="#dog-announcements"
 | 
			
		||||
    [[gateway.in]]
 | 
			
		||||
    account="irc.bar"
 | 
			
		||||
    channel="#dog-announcements"
 | 
			
		||||
    [[gateway.out]]
 | 
			
		||||
    account="telegram.mytelegram"
 | 
			
		||||
    channel="-9876543219876"
 | 
			
		||||
    [[gateway.out]]
 | 
			
		||||
    account="mattermost.work"
 | 
			
		||||
    channel="dog-announcements"
 | 
			
		||||
@@ -24,6 +24,13 @@ Password=""
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
UseTLS=false
 | 
			
		||||
 | 
			
		||||
#Use client certificate - see CertFP https://libera.chat/guides/certfp.html
 | 
			
		||||
#Specify filename which contains private key and cert
 | 
			
		||||
#OPTIONAL (default "")
 | 
			
		||||
#
 | 
			
		||||
#TLSClientCertificate="cert.pem"
 | 
			
		||||
TLSClientCertificate=""
 | 
			
		||||
 | 
			
		||||
#Enable SASL (PLAIN) authentication. (libera requires this from eg AWS hosts)
 | 
			
		||||
#It uses NickServNick and NickServPassword as login and password
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
@@ -34,6 +41,11 @@ UseSASL=false
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
SkipTLSVerify=true
 | 
			
		||||
 | 
			
		||||
#Local address to use for server connection
 | 
			
		||||
#Note that Server and Bind must resolve to addresses of the same family.
 | 
			
		||||
#OPTIONAL (default "")
 | 
			
		||||
Bind=""
 | 
			
		||||
 | 
			
		||||
#If you know your charset, you can specify it manually.
 | 
			
		||||
#Otherwise it tries to detect this automatically. Select one below
 | 
			
		||||
# "iso-8859-2:1987", "iso-8859-9:1989", "866", "latin9", "iso-8859-10:1992", "iso-ir-109", "hebrew",
 | 
			
		||||
@@ -110,10 +122,11 @@ RejoinDelay=0
 | 
			
		||||
#Only works in IRC right now.
 | 
			
		||||
ColorNicks=false
 | 
			
		||||
 | 
			
		||||
#RunCommands allows you to send RAW irc commands after connection
 | 
			
		||||
#RunCommands allows you to send RAW irc commands after connection.
 | 
			
		||||
#The string {BOTNICK} (case sensitive) will be replaced with the bot's current nickname.
 | 
			
		||||
#Array of strings
 | 
			
		||||
#OPTIONAL (default empty)
 | 
			
		||||
RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"]
 | 
			
		||||
RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something", "MODE {BOTNICK} +B"]
 | 
			
		||||
 | 
			
		||||
#PingDelay specifies how long to wait to send a ping to the irc server.
 | 
			
		||||
#You can use s for second, m for minute
 | 
			
		||||
@@ -150,7 +163,7 @@ IgnoreMessages="^~~ badword"
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#nicks you want to replace.
 | 
			
		||||
#see replacemessages for syntaxa
 | 
			
		||||
#see replacemessages for syntax
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
@@ -175,7 +188,7 @@ Label=""
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
@@ -185,7 +198,7 @@ ShowJoinPart=false
 | 
			
		||||
VerboseJoinPart=false
 | 
			
		||||
 | 
			
		||||
#Do not send joins/parts to other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
NoSendJoinPart=false
 | 
			
		||||
 | 
			
		||||
@@ -288,7 +301,7 @@ IgnoreMessages="^~~ badword"
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#Nicks you want to replace.
 | 
			
		||||
#See ReplaceMessages for syntaxA
 | 
			
		||||
#See ReplaceMessages for syntax
 | 
			
		||||
#OPTIONAL (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
@@ -312,7 +325,7 @@ Label=""
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
@@ -396,6 +409,10 @@ SkipTLSVerify=true
 | 
			
		||||
## RELOADABLE SETTINGS
 | 
			
		||||
## Settings below can be reloaded by editing the file
 | 
			
		||||
 | 
			
		||||
# UseUserName shows the username instead of the server nickname
 | 
			
		||||
# OPTIONAL (default false)
 | 
			
		||||
UseUserName=false
 | 
			
		||||
 | 
			
		||||
#how to format the list of IRC nicks when displayed in mattermost.
 | 
			
		||||
#Possible options are "table" and "plain"
 | 
			
		||||
#OPTIONAL (default plain)
 | 
			
		||||
@@ -451,7 +468,7 @@ IgnoreMessages="^~~ badword"
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#nicks you want to replace.
 | 
			
		||||
#see replacemessages for syntaxa
 | 
			
		||||
#see replacemessages for syntax
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
@@ -475,12 +492,12 @@ Label=""
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
#Do not send joins/parts to other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
NoSendJoinPart=false
 | 
			
		||||
 | 
			
		||||
@@ -496,86 +513,9 @@ ShowTopicChange=false
 | 
			
		||||
 | 
			
		||||
###################################################################
 | 
			
		||||
#Gitter section
 | 
			
		||||
#Best to make a dedicated gitter account for the bot.
 | 
			
		||||
#Gitter has been moved to matrix - see https://github.com/42wim/matterbridge/issues/1969 how to migrate
 | 
			
		||||
###################################################################
 | 
			
		||||
 | 
			
		||||
[gitter]
 | 
			
		||||
 | 
			
		||||
#You can configure multiple servers "[gitter.name]" or "[gitter.name2]"
 | 
			
		||||
#In this example we use [gitter.myproject]
 | 
			
		||||
#REQUIRED
 | 
			
		||||
[gitter.myproject]
 | 
			
		||||
#Token to connect with Gitter API
 | 
			
		||||
#You can get your token by going to https://developer.gitter.im/docs/welcome and SIGN IN
 | 
			
		||||
#REQUIRED
 | 
			
		||||
Token="Yourtokenhere"
 | 
			
		||||
 | 
			
		||||
## RELOADABLE SETTINGS
 | 
			
		||||
## Settings below can be reloaded by editing the file
 | 
			
		||||
 | 
			
		||||
#Nicks you want to ignore.
 | 
			
		||||
#Regular expressions supported
 | 
			
		||||
#Messages from those users will not be sent to other bridges.
 | 
			
		||||
#OPTIONAL
 | 
			
		||||
IgnoreNicks="ircspammer1 ircspammer2"
 | 
			
		||||
 | 
			
		||||
#Messages you want to ignore.
 | 
			
		||||
#Messages matching these regexp will be ignored and not sent to other bridges
 | 
			
		||||
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
 | 
			
		||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
 | 
			
		||||
IgnoreMessages="^~~ badword"
 | 
			
		||||
 | 
			
		||||
#messages you want to replace.
 | 
			
		||||
#it replaces outgoing messages from the bridge.
 | 
			
		||||
#so you need to place it by the sending bridge definition.
 | 
			
		||||
#regular expressions supported
 | 
			
		||||
#some examples:
 | 
			
		||||
#this replaces cat => dog and sleep => awake
 | 
			
		||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
 | 
			
		||||
#this replaces every number with number.  123 => numbernumbernumber
 | 
			
		||||
#replacemessages=[ ["[0-9]","number"] ]
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#nicks you want to replace.
 | 
			
		||||
#see replacemessages for syntaxa
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
#Extractnicks is used to for example rewrite messages from other relaybots
 | 
			
		||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
 | 
			
		||||
#some examples:
 | 
			
		||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
 | 
			
		||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
 | 
			
		||||
#you can use multiple entries for multiplebots
 | 
			
		||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
 | 
			
		||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
 | 
			
		||||
#OPTIONAL (default empty)
 | 
			
		||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
 | 
			
		||||
 | 
			
		||||
#extra label that can be used in the RemoteNickFormat
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
Label=""
 | 
			
		||||
 | 
			
		||||
#RemoteNickFormat defines how remote users appear on this bridge
 | 
			
		||||
#See [general] config section for default options
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
 | 
			
		||||
#It will strip other characters from the nick
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
StripNick=false
 | 
			
		||||
 | 
			
		||||
#Enable to show topic changes from other bridges
 | 
			
		||||
#Only works hiding/show topic changes from slack bridge for now
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowTopicChange=false
 | 
			
		||||
 | 
			
		||||
###################################################################
 | 
			
		||||
#
 | 
			
		||||
# Keybase
 | 
			
		||||
@@ -646,7 +586,7 @@ IgnoreMessages="^~~ badword"
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#nicks you want to replace.
 | 
			
		||||
#see replacemessages for syntaxa
 | 
			
		||||
#see replacemessages for syntax
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
@@ -670,7 +610,7 @@ Label=""
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
@@ -785,7 +725,7 @@ IgnoreMessages="^~~ badword"
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#nicks you want to replace.
 | 
			
		||||
#see replacemessages for syntaxa
 | 
			
		||||
#see replacemessages for syntax
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
@@ -809,12 +749,12 @@ Label=""
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
#Do not send joins/parts to other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
NoSendJoinPart=false
 | 
			
		||||
 | 
			
		||||
@@ -846,6 +786,10 @@ ShowUserTyping=false
 | 
			
		||||
#Default "<clipped message>"
 | 
			
		||||
MessageClipped="<clipped message>"
 | 
			
		||||
 | 
			
		||||
#If enabled use the slack "Real Name" as username.
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
UseFullName=false
 | 
			
		||||
 | 
			
		||||
###################################################################
 | 
			
		||||
#discord section
 | 
			
		||||
###################################################################
 | 
			
		||||
@@ -981,10 +925,17 @@ ShowTopicChange=false
 | 
			
		||||
# Supported from the following bridges: slack
 | 
			
		||||
SyncTopic=false
 | 
			
		||||
 | 
			
		||||
#Message to show when a message is too big
 | 
			
		||||
#Default "<clipped message>"
 | 
			
		||||
# Message to show when a message is too big
 | 
			
		||||
# Default "<clipped message>"
 | 
			
		||||
MessageClipped="<clipped message>"
 | 
			
		||||
 | 
			
		||||
# Before clipping, try to split messages into at most this many parts. 0 is treated like 1.
 | 
			
		||||
# Be careful with large numbers, as this might cause flooding.
 | 
			
		||||
# Example: A maximum telegram message of 4096 bytes is received. This requires 3 Discord
 | 
			
		||||
# messages (each capped at a hardcoded 1950 bytes).
 | 
			
		||||
# Default 1
 | 
			
		||||
MessageSplitMaxCount=3
 | 
			
		||||
 | 
			
		||||
###################################################################
 | 
			
		||||
#telegram section
 | 
			
		||||
###################################################################
 | 
			
		||||
@@ -1020,6 +971,12 @@ DisableWebPagePreview=false
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
UseFirstName=false
 | 
			
		||||
 | 
			
		||||
#If enabled use the "Full Name" as username. If this is empty use the Username
 | 
			
		||||
#If disabled use the "Username" as username. If this is empty use the First Name and Last Name as Full Name
 | 
			
		||||
#If all names are empty, username will be "unknown"
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
UseFullName=false
 | 
			
		||||
 | 
			
		||||
#WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs
 | 
			
		||||
#Those URLs will contain your bot-token. This may not be what you want.
 | 
			
		||||
#For now there is no secure way to relay GIF/stickers/documents without seeing your token.
 | 
			
		||||
@@ -1083,7 +1040,7 @@ IgnoreMessages="^~~ badword"
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#nicks you want to replace.
 | 
			
		||||
#see replacemessages for syntaxa
 | 
			
		||||
#see replacemessages for syntax
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
@@ -1111,7 +1068,7 @@ Label=""
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
@@ -1125,6 +1082,12 @@ StripNick=false
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowTopicChange=false
 | 
			
		||||
 | 
			
		||||
#Opportunistically preserve threaded replies between Telegram groups.
 | 
			
		||||
#This only works if the parent message is still in the cache.
 | 
			
		||||
#Cache is flushed between restarts.
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
PreserveThreading=false
 | 
			
		||||
 | 
			
		||||
###################################################################
 | 
			
		||||
#rocketchat section
 | 
			
		||||
###################################################################
 | 
			
		||||
@@ -1218,7 +1181,7 @@ IgnoreMessages="^~~ badword"
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#nicks you want to replace.
 | 
			
		||||
#see replacemessages for syntaxa
 | 
			
		||||
#see replacemessages for syntax
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
@@ -1242,7 +1205,7 @@ Label=""
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
@@ -1296,6 +1259,15 @@ HTMLDisable=false
 | 
			
		||||
# UseUserName shows the username instead of the server nickname
 | 
			
		||||
UseUserName=false
 | 
			
		||||
 | 
			
		||||
# Matrix quotes replies and as of matterbridge 1.24.0 we strip those as this causes
 | 
			
		||||
# issues with bridges support threading and have PreserveThreading enabled.
 | 
			
		||||
# But if you for example use mattermost or discord with webhooks you'll need to enable 
 | 
			
		||||
# this (and keep PreserveThreading disabled) if you want something that looks like a reply from matrix.
 | 
			
		||||
# See issues: 
 | 
			
		||||
# - https://github.com/42wim/matterbridge/issues/1819
 | 
			
		||||
# - https://github.com/42wim/matterbridge/issues/1780
 | 
			
		||||
KeepQuotedReply=false
 | 
			
		||||
 | 
			
		||||
#Nicks you want to ignore.
 | 
			
		||||
#Regular expressions supported
 | 
			
		||||
#Messages from those users will not be sent to other bridges.
 | 
			
		||||
@@ -1321,7 +1293,7 @@ IgnoreMessages="^~~ badword"
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#nicks you want to replace.
 | 
			
		||||
#see replacemessages for syntaxa
 | 
			
		||||
#see replacemessages for syntax
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
@@ -1345,10 +1317,15 @@ Label=""
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
#Rename the bot in the current room to the username of the message
 | 
			
		||||
#This will make an additional API request per message and will probably count towards rate limits
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
SpoofUsername=false
 | 
			
		||||
 | 
			
		||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
 | 
			
		||||
#It will strip other characters from the nick
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
@@ -1413,7 +1390,7 @@ IgnoreMessages="^~~ badword"
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#nicks you want to replace.
 | 
			
		||||
#see replacemessages for syntaxa
 | 
			
		||||
#see replacemessages for syntax
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
@@ -1437,7 +1414,7 @@ Label=""
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
@@ -1525,6 +1502,15 @@ SkipTLSVerify=false
 | 
			
		||||
#Default "<clipped message>"
 | 
			
		||||
MessageClipped="<clipped message>"
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
#Do not send joins/parts to other bridges
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
NoSendJoinPart=false
 | 
			
		||||
 | 
			
		||||
###################################################################
 | 
			
		||||
#VK
 | 
			
		||||
###################################################################
 | 
			
		||||
@@ -1534,10 +1520,6 @@ MessageClipped="<clipped message>"
 | 
			
		||||
#See https://vk.com/dev/bots_docs
 | 
			
		||||
Token="Yourtokenhere"
 | 
			
		||||
 | 
			
		||||
#Group ID
 | 
			
		||||
#For example in URL https://vk.com/public168963511 group ID is 168963511
 | 
			
		||||
GroupID=123456789
 | 
			
		||||
 | 
			
		||||
###################################################################
 | 
			
		||||
# WhatsApp
 | 
			
		||||
###################################################################
 | 
			
		||||
@@ -1617,7 +1599,7 @@ IgnoreMessages="^~~ badword"
 | 
			
		||||
ReplaceMessages=[ ["cat","dog"] ]
 | 
			
		||||
 | 
			
		||||
#nicks you want to replace.
 | 
			
		||||
#see replacemessages for syntaxa
 | 
			
		||||
#see replacemessages for syntax
 | 
			
		||||
#optional (default empty)
 | 
			
		||||
ReplaceNicks=[ ["user--","user"] ]
 | 
			
		||||
 | 
			
		||||
@@ -1641,7 +1623,7 @@ Label=""
 | 
			
		||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
#Enable to show users joins/parts from other bridges
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
 | 
			
		||||
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowJoinPart=false
 | 
			
		||||
 | 
			
		||||
@@ -1655,6 +1637,18 @@ StripNick=false
 | 
			
		||||
#OPTIONAL (default false)
 | 
			
		||||
ShowTopicChange=false
 | 
			
		||||
 | 
			
		||||
###################################################################
 | 
			
		||||
# Harmony
 | 
			
		||||
###################################################################
 | 
			
		||||
 | 
			
		||||
[harmony.chat_harmonyapp_io]
 | 
			
		||||
Homeserver = "https://chat.harmonyapp.io:2289"
 | 
			
		||||
Token = "your token goes here"
 | 
			
		||||
UserID = "user id of the bot account"
 | 
			
		||||
Community = "community id that channels will be located in"
 | 
			
		||||
UseUserName = true
 | 
			
		||||
RemoteNickFormat = "{NICK}"
 | 
			
		||||
 | 
			
		||||
###################################################################
 | 
			
		||||
#API
 | 
			
		||||
###################################################################
 | 
			
		||||
@@ -1700,6 +1694,7 @@ RemoteNickFormat="{NICK}"
 | 
			
		||||
 | 
			
		||||
#RemoteNickFormat defines how remote users appear on this bridge
 | 
			
		||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick.
 | 
			
		||||
#The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged.
 | 
			
		||||
#The string "{USERID}" (case sensitive) will be replaced by the user ID.
 | 
			
		||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
 | 
			
		||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
 | 
			
		||||
@@ -1872,7 +1867,8 @@ enable=true
 | 
			
		||||
    # -------------------------------------------------------------------------------------------------------------------------------------
 | 
			
		||||
    #    irc     |      channel       |            #general           | The # symbol is required and should be lowercase!
 | 
			
		||||
    # -------------------------------------------------------------------------------------------------------------------------------------
 | 
			
		||||
    # mattermost |      channel       |            general            | This is the channel name as seen in the URL, not the display name
 | 
			
		||||
    #            |      channel       |            general            | This is the channel name as seen in the URL, not the display name
 | 
			
		||||
    # mattermost |    channel id      | ID:oc4wifyuojgw5f3nsuweesmz8w | This is the channel ID (only use if you know what you're doing)
 | 
			
		||||
    # -------------------------------------------------------------------------------------------------------------------------------------
 | 
			
		||||
    #   matrix   | #channel:server    |    #yourchannel:matrix.org    | Encrypted rooms are not supported in matrix
 | 
			
		||||
    # -------------------------------------------------------------------------------------------------------------------------------------
 | 
			
		||||
@@ -1890,6 +1886,7 @@ enable=true
 | 
			
		||||
    #   nctalk   |      token         |           xs25tz5y            | The token in the URL when you are in a chat. It will be the last part of the URL.
 | 
			
		||||
    # -------------------------------------------------------------------------------------------------------------------------------------
 | 
			
		||||
    #  telegram  |      chatid        |          -123456789           | A large negative number. see https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau
 | 
			
		||||
    #            |   chatid/topicid   |          -123456789/12        | A large negative number/number.
 | 
			
		||||
    # -------------------------------------------------------------------------------------------------------------------------------------
 | 
			
		||||
    #  vk        |      peerid        |          2000000002           | A number that starts form 2000000000. Use --debug and send any message in chat to get PeerID in the logs
 | 
			
		||||
    # -------------------------------------------------------------------------------------------------------------------------------------
 | 
			
		||||
@@ -1898,7 +1895,7 @@ enable=true
 | 
			
		||||
    # -------------------------------------------------------------------------------------------------------------------------------------
 | 
			
		||||
    #    xmpp    |      channel       |            general            | The room name
 | 
			
		||||
    # -------------------------------------------------------------------------------------------------------------------------------------
 | 
			
		||||
    #   zulip    | stream/topic:topic |     general/off-topic:food    | Do not use the # when specifying a topic
 | 
			
		||||
    #   zulip    | stream/topic:topic |      general/topic:food       | Do not use the # when specifying a topic
 | 
			
		||||
    # -------------------------------------------------------------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
@@ -1947,6 +1944,10 @@ enable=true
 | 
			
		||||
    account="zulip.streamchat"
 | 
			
		||||
    channel="general/topic:mytopic"
 | 
			
		||||
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
    account="harmony.chat_harmonyapp_io"
 | 
			
		||||
    channel="channel id goes here"
 | 
			
		||||
 | 
			
		||||
    #API example
 | 
			
		||||
    #[[gateway.inout]]
 | 
			
		||||
    #account="api.local"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#WARNING: as this file contains credentials, be sure to set correct file permissions
 | 
			
		||||
[irc]
 | 
			
		||||
    [irc.freenode]
 | 
			
		||||
    Server="irc.freenode.net:6667"
 | 
			
		||||
    [irc.libera]
 | 
			
		||||
    Server="irc.libera.chat:6667"
 | 
			
		||||
    Nick="matterbot"
 | 
			
		||||
 | 
			
		||||
[mattermost]
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
name="gateway1"
 | 
			
		||||
enable=true
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
    account="irc.freenode"
 | 
			
		||||
    account="irc.libera"
 | 
			
		||||
    channel="#testing"
 | 
			
		||||
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
@@ -29,6 +29,6 @@ enable=true
 | 
			
		||||
#name="gateway2"
 | 
			
		||||
#enable=true
 | 
			
		||||
#inout = [
 | 
			
		||||
#    { account="irc.freenode", channel="#testing", options={key="channelkey"}},
 | 
			
		||||
#    { account="irc.libera", channel="#testing", options={key="channelkey"}},
 | 
			
		||||
#    { account="mattermost.work", channel="off-topic" },
 | 
			
		||||
#]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								matterclient/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								matterclient/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
Find matterclient on https://github.com/matterbridge/matterclient
 | 
			
		||||
@@ -1,226 +0,0 @@
 | 
			
		||||
package matterclient
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattermost/mattermost-server/v5/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetChannels returns all channels we're members off
 | 
			
		||||
func (m *MMClient) GetChannels() []*model.Channel {
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	var channels []*model.Channel
 | 
			
		||||
	// our primary team channels first
 | 
			
		||||
	channels = append(channels, m.Team.Channels...)
 | 
			
		||||
	for _, t := range m.OtherTeams {
 | 
			
		||||
		if t.Id != m.Team.Id {
 | 
			
		||||
			channels = append(channels, t.Channels...)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return channels
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetChannelHeader(channelId string) string { //nolint:golint
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	for _, t := range m.OtherTeams {
 | 
			
		||||
		for _, channel := range append(t.Channels, t.MoreChannels...) {
 | 
			
		||||
			if channel.Id == channelId {
 | 
			
		||||
				return channel.Header
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getNormalisedName(channel *model.Channel) string {
 | 
			
		||||
	if channel.Type == model.CHANNEL_GROUP {
 | 
			
		||||
		// (deprecated in favor of ReplaceAll in go 1.12)
 | 
			
		||||
		res := strings.Replace(channel.DisplayName, ", ", "-", -1) //nolint: gocritic
 | 
			
		||||
		res = strings.Replace(res, " ", "_", -1)                   //nolint: gocritic
 | 
			
		||||
		return res
 | 
			
		||||
	}
 | 
			
		||||
	return channel.Name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	if teamId != "" {
 | 
			
		||||
		return m.getChannelIdTeam(name, teamId)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, t := range m.OtherTeams {
 | 
			
		||||
		for _, channel := range append(t.Channels, t.MoreChannels...) {
 | 
			
		||||
			if getNormalisedName(channel) == name {
 | 
			
		||||
				return channel.Id
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) getChannelIdTeam(name string, teamId string) string { //nolint:golint
 | 
			
		||||
	for _, t := range m.OtherTeams {
 | 
			
		||||
		if t.Id == teamId {
 | 
			
		||||
			for _, channel := range append(t.Channels, t.MoreChannels...) {
 | 
			
		||||
				if getNormalisedName(channel) == name {
 | 
			
		||||
					return channel.Id
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetChannelName(channelId string) string { //nolint:golint
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	for _, t := range m.OtherTeams {
 | 
			
		||||
		if t == nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		for _, channel := range append(t.Channels, t.MoreChannels...) {
 | 
			
		||||
			if channel.Id == channelId {
 | 
			
		||||
				return getNormalisedName(channel)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetChannelTeamId(id string) string { //nolint:golint
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	for _, t := range append(m.OtherTeams, m.Team) {
 | 
			
		||||
		for _, channel := range append(t.Channels, t.MoreChannels...) {
 | 
			
		||||
			if channel.Id == id {
 | 
			
		||||
				return channel.TeamId
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetLastViewedAt(channelId string) int64 { //nolint:golint
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "")
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return model.GetMillis()
 | 
			
		||||
	}
 | 
			
		||||
	return res.LastViewedAt
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetMoreChannels returns existing channels where we're not a member off.
 | 
			
		||||
func (m *MMClient) GetMoreChannels() []*model.Channel {
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	var channels []*model.Channel
 | 
			
		||||
	for _, t := range m.OtherTeams {
 | 
			
		||||
		channels = append(channels, t.MoreChannels...)
 | 
			
		||||
	}
 | 
			
		||||
	return channels
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId).
 | 
			
		||||
func (m *MMClient) GetTeamFromChannel(channelId string) string { //nolint:golint
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	var channels []*model.Channel
 | 
			
		||||
	for _, t := range m.OtherTeams {
 | 
			
		||||
		channels = append(channels, t.Channels...)
 | 
			
		||||
		if t.MoreChannels != nil {
 | 
			
		||||
			channels = append(channels, t.MoreChannels...)
 | 
			
		||||
		}
 | 
			
		||||
		for _, c := range channels {
 | 
			
		||||
			if c.Id == channelId {
 | 
			
		||||
				if c.Type == model.CHANNEL_GROUP {
 | 
			
		||||
					return "G"
 | 
			
		||||
				}
 | 
			
		||||
				return t.Id
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		channels = nil
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	for _, c := range m.Team.Channels {
 | 
			
		||||
		if c.Id == channelId {
 | 
			
		||||
			m.logger.Debug("Not joining ", channelId, " already joined.")
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	m.logger.Debug("Joining ", channelId)
 | 
			
		||||
	_, resp := m.Client.AddChannelMember(channelId, m.User.Id)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) UpdateChannelsTeam(teamID string) error {
 | 
			
		||||
	mmchannels, resp := m.Client.GetChannelsForTeamForUser(teamID, m.User.Id, false, "")
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return errors.New(resp.Error.DetailedError)
 | 
			
		||||
	}
 | 
			
		||||
	for idx, t := range m.OtherTeams {
 | 
			
		||||
		if t.Id == teamID {
 | 
			
		||||
			m.Lock()
 | 
			
		||||
			m.OtherTeams[idx].Channels = mmchannels
 | 
			
		||||
			m.Unlock()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mmchannels, resp = m.Client.GetPublicChannelsForTeam(teamID, 0, 5000, "")
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return errors.New(resp.Error.DetailedError)
 | 
			
		||||
	}
 | 
			
		||||
	for idx, t := range m.OtherTeams {
 | 
			
		||||
		if t.Id == teamID {
 | 
			
		||||
			m.Lock()
 | 
			
		||||
			m.OtherTeams[idx].MoreChannels = mmchannels
 | 
			
		||||
			m.Unlock()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) UpdateChannels() error {
 | 
			
		||||
	if err := m.UpdateChannelsTeam(m.Team.Id); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for _, t := range m.OtherTeams {
 | 
			
		||||
		if err := m.UpdateChannelsTeam(t.Id); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) UpdateChannelHeader(channelId string, header string) { //nolint:golint
 | 
			
		||||
	channel := &model.Channel{Id: channelId, Header: header}
 | 
			
		||||
	m.logger.Debugf("updating channelheader %#v, %#v", channelId, header)
 | 
			
		||||
	_, resp := m.Client.UpdateChannel(channel)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		m.logger.Error(resp.Error)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) UpdateLastViewed(channelId string) error { //nolint:golint
 | 
			
		||||
	m.logger.Debugf("posting lastview %#v", channelId)
 | 
			
		||||
	view := &model.ChannelView{ChannelId: channelId}
 | 
			
		||||
	_, resp := m.Client.ViewChannel(m.User.Id, view)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		m.logger.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error)
 | 
			
		||||
		return resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,297 +0,0 @@
 | 
			
		||||
package matterclient
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/md5" //nolint:gosec
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/cookiejar"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"github.com/jpillora/backoff"
 | 
			
		||||
	"github.com/mattermost/mattermost-server/v5/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error {
 | 
			
		||||
	var resp *model.Response
 | 
			
		||||
	var appErr *model.AppError
 | 
			
		||||
	var logmsg = "trying login"
 | 
			
		||||
	var err error
 | 
			
		||||
	for {
 | 
			
		||||
		m.logger.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
 | 
			
		||||
		if m.Credentials.Token != "" {
 | 
			
		||||
			resp, err = m.doLoginToken()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
 | 
			
		||||
		}
 | 
			
		||||
		appErr = resp.Error
 | 
			
		||||
		if appErr != nil {
 | 
			
		||||
			d := b.Duration()
 | 
			
		||||
			m.logger.Debug(appErr.DetailedError)
 | 
			
		||||
			if firstConnection {
 | 
			
		||||
				if appErr.Message == "" {
 | 
			
		||||
					return errors.New(appErr.DetailedError)
 | 
			
		||||
				}
 | 
			
		||||
				return errors.New(appErr.Message)
 | 
			
		||||
			}
 | 
			
		||||
			m.logger.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
 | 
			
		||||
			time.Sleep(d)
 | 
			
		||||
			logmsg = "retrying login"
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
	// reset timer
 | 
			
		||||
	b.Reset()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) doLoginToken() (*model.Response, error) {
 | 
			
		||||
	var resp *model.Response
 | 
			
		||||
	var logmsg = "trying login"
 | 
			
		||||
	m.Client.AuthType = model.HEADER_BEARER
 | 
			
		||||
	m.Client.AuthToken = m.Credentials.Token
 | 
			
		||||
	if m.Credentials.CookieToken {
 | 
			
		||||
		m.logger.Debugf(logmsg + " with cookie (MMAUTH) token")
 | 
			
		||||
		m.Client.HttpClient.Jar = m.createCookieJar(m.Credentials.Token)
 | 
			
		||||
	} else {
 | 
			
		||||
		m.logger.Debugf(logmsg + " with personal token")
 | 
			
		||||
	}
 | 
			
		||||
	m.User, resp = m.Client.GetMe("")
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return resp, resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	if m.User == nil {
 | 
			
		||||
		m.logger.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
 | 
			
		||||
		return resp, errors.New("invalid token")
 | 
			
		||||
	}
 | 
			
		||||
	return resp, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) handleLoginToken() error {
 | 
			
		||||
	switch {
 | 
			
		||||
	case strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN):
 | 
			
		||||
		token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
 | 
			
		||||
		if len(token) != 2 {
 | 
			
		||||
			return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
 | 
			
		||||
		}
 | 
			
		||||
		m.Credentials.Token = token[1]
 | 
			
		||||
		m.Credentials.CookieToken = true
 | 
			
		||||
	case strings.Contains(m.Credentials.Pass, "token="):
 | 
			
		||||
		token := strings.Split(m.Credentials.Pass, "token=")
 | 
			
		||||
		if len(token) != 2 {
 | 
			
		||||
			return errors.New("incorrect personal token. valid input is token=yourtoken")
 | 
			
		||||
		}
 | 
			
		||||
		m.Credentials.Token = token[1]
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) initClient(firstConnection bool, b *backoff.Backoff) error {
 | 
			
		||||
	uriScheme := "https://"
 | 
			
		||||
	if m.NoTLS {
 | 
			
		||||
		uriScheme = "http://"
 | 
			
		||||
	}
 | 
			
		||||
	// login to mattermost
 | 
			
		||||
	m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server)
 | 
			
		||||
	m.Client.HttpClient.Transport = &http.Transport{
 | 
			
		||||
		TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
 | 
			
		||||
		Proxy:           http.ProxyFromEnvironment,
 | 
			
		||||
	}
 | 
			
		||||
	m.Client.HttpClient.Timeout = time.Second * 10
 | 
			
		||||
 | 
			
		||||
	// handle MMAUTHTOKEN and personal token
 | 
			
		||||
	if err := m.handleLoginToken(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check if server alive, retry until
 | 
			
		||||
	if err := m.serverAlive(firstConnection, b); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// initialize user and teams
 | 
			
		||||
func (m *MMClient) initUser() error {
 | 
			
		||||
	m.Lock()
 | 
			
		||||
	defer m.Unlock()
 | 
			
		||||
	// we only load all team data on initial login.
 | 
			
		||||
	// all other updates are for channels from our (primary) team only.
 | 
			
		||||
	//m.logger.Debug("initUser(): loading all team data")
 | 
			
		||||
	teams, resp := m.Client.GetTeamsForUser(m.User.Id, "")
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	for _, team := range teams {
 | 
			
		||||
		idx := 0
 | 
			
		||||
		max := 200
 | 
			
		||||
		usermap := make(map[string]*model.User)
 | 
			
		||||
		mmusers, resp := m.Client.GetUsersInTeam(team.Id, idx, max, "")
 | 
			
		||||
		if resp.Error != nil {
 | 
			
		||||
			return errors.New(resp.Error.DetailedError)
 | 
			
		||||
		}
 | 
			
		||||
		for len(mmusers) > 0 {
 | 
			
		||||
			for _, user := range mmusers {
 | 
			
		||||
				usermap[user.Id] = user
 | 
			
		||||
			}
 | 
			
		||||
			mmusers, resp = m.Client.GetUsersInTeam(team.Id, idx, max, "")
 | 
			
		||||
			if resp.Error != nil {
 | 
			
		||||
				return errors.New(resp.Error.DetailedError)
 | 
			
		||||
			}
 | 
			
		||||
			idx++
 | 
			
		||||
			time.Sleep(time.Millisecond * 200)
 | 
			
		||||
		}
 | 
			
		||||
		m.logger.Infof("found %d users in team %s", len(usermap), team.Name)
 | 
			
		||||
 | 
			
		||||
		t := &Team{Team: team, Users: usermap, Id: team.Id}
 | 
			
		||||
 | 
			
		||||
		mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, false, "")
 | 
			
		||||
		if resp.Error != nil {
 | 
			
		||||
			return resp.Error
 | 
			
		||||
		}
 | 
			
		||||
		t.Channels = mmchannels
 | 
			
		||||
		mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "")
 | 
			
		||||
		if resp.Error != nil {
 | 
			
		||||
			return resp.Error
 | 
			
		||||
		}
 | 
			
		||||
		t.MoreChannels = mmchannels
 | 
			
		||||
		m.OtherTeams = append(m.OtherTeams, t)
 | 
			
		||||
		if team.Name == m.Credentials.Team {
 | 
			
		||||
			m.Team = t
 | 
			
		||||
			m.logger.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id)
 | 
			
		||||
		}
 | 
			
		||||
		// add all users
 | 
			
		||||
		for k, v := range t.Users {
 | 
			
		||||
			m.Users[k] = v
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error {
 | 
			
		||||
	defer b.Reset()
 | 
			
		||||
	for {
 | 
			
		||||
		d := b.Duration()
 | 
			
		||||
		// bogus call to get the serverversion
 | 
			
		||||
		_, resp := m.Client.Logout()
 | 
			
		||||
		if resp.Error != nil {
 | 
			
		||||
			return fmt.Errorf("%#v", resp.Error.Error())
 | 
			
		||||
		}
 | 
			
		||||
		if firstConnection && !m.SkipVersionCheck && !supportedVersion(resp.ServerVersion) {
 | 
			
		||||
			return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion)
 | 
			
		||||
		}
 | 
			
		||||
		if !m.SkipVersionCheck {
 | 
			
		||||
			m.ServerVersion = resp.ServerVersion
 | 
			
		||||
			if m.ServerVersion == "" {
 | 
			
		||||
				m.logger.Debugf("Server not up yet, reconnecting in %s", d)
 | 
			
		||||
				time.Sleep(d)
 | 
			
		||||
			} else {
 | 
			
		||||
				m.logger.Infof("Found version %s", m.ServerVersion)
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) wsConnect() {
 | 
			
		||||
	b := &backoff.Backoff{
 | 
			
		||||
		Min:    time.Second,
 | 
			
		||||
		Max:    5 * time.Minute,
 | 
			
		||||
		Jitter: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.WsConnected = false
 | 
			
		||||
	wsScheme := "wss://"
 | 
			
		||||
	if m.NoTLS {
 | 
			
		||||
		wsScheme = "ws://"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// setup websocket connection
 | 
			
		||||
	wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket"
 | 
			
		||||
	header := http.Header{}
 | 
			
		||||
	header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
 | 
			
		||||
 | 
			
		||||
	m.logger.Debugf("WsClient: making connection: %s", wsurl)
 | 
			
		||||
	for {
 | 
			
		||||
		wsDialer := &websocket.Dialer{
 | 
			
		||||
			TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
 | 
			
		||||
			Proxy:           http.ProxyFromEnvironment,
 | 
			
		||||
		}
 | 
			
		||||
		var err error
 | 
			
		||||
		m.WsClient, _, err = wsDialer.Dial(wsurl, header)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			d := b.Duration()
 | 
			
		||||
			m.logger.Debugf("WSS: %s, reconnecting in %s", err, d)
 | 
			
		||||
			time.Sleep(d)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.logger.Debug("WsClient: connected")
 | 
			
		||||
	m.WsSequence = 1
 | 
			
		||||
	m.WsPingChan = make(chan *model.WebSocketResponse)
 | 
			
		||||
	// only start to parse WS messages when login is completely done
 | 
			
		||||
	m.WsConnected = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
 | 
			
		||||
	var cookies []*http.Cookie
 | 
			
		||||
	jar, _ := cookiejar.New(nil)
 | 
			
		||||
	firstCookie := &http.Cookie{
 | 
			
		||||
		Name:   "MMAUTHTOKEN",
 | 
			
		||||
		Value:  token,
 | 
			
		||||
		Path:   "/",
 | 
			
		||||
		Domain: m.Credentials.Server,
 | 
			
		||||
	}
 | 
			
		||||
	cookies = append(cookies, firstCookie)
 | 
			
		||||
	cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
 | 
			
		||||
	jar.SetCookies(cookieURL, cookies)
 | 
			
		||||
	return jar
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) checkAlive() error {
 | 
			
		||||
	// check if session still is valid
 | 
			
		||||
	_, resp := m.Client.GetMe("")
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	m.logger.Debug("WS PING")
 | 
			
		||||
	return m.sendWSRequest("ping", nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error {
 | 
			
		||||
	req := &model.WebSocketRequest{}
 | 
			
		||||
	req.Seq = m.WsSequence
 | 
			
		||||
	req.Action = action
 | 
			
		||||
	req.Data = data
 | 
			
		||||
	m.WsSequence++
 | 
			
		||||
	m.logger.Debugf("sendWsRequest %#v", req)
 | 
			
		||||
	return m.WsClient.WriteJSON(req)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func supportedVersion(version string) bool {
 | 
			
		||||
	if strings.HasPrefix(version, "3.8.0") ||
 | 
			
		||||
		strings.HasPrefix(version, "3.9.0") ||
 | 
			
		||||
		strings.HasPrefix(version, "3.10.0") ||
 | 
			
		||||
		strings.HasPrefix(version, "4.") ||
 | 
			
		||||
		strings.HasPrefix(version, "5.") {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func digestString(s string) string {
 | 
			
		||||
	return fmt.Sprintf("%x", md5.Sum([]byte(s))) //nolint:gosec
 | 
			
		||||
}
 | 
			
		||||
@@ -1,294 +0,0 @@
 | 
			
		||||
package matterclient
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	lru "github.com/hashicorp/golang-lru"
 | 
			
		||||
	"github.com/jpillora/backoff"
 | 
			
		||||
	prefixed "github.com/matterbridge/logrus-prefixed-formatter"
 | 
			
		||||
	"github.com/mattermost/mattermost-server/v5/model"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Credentials struct {
 | 
			
		||||
	Login            string
 | 
			
		||||
	Team             string
 | 
			
		||||
	Pass             string
 | 
			
		||||
	Token            string
 | 
			
		||||
	CookieToken      bool
 | 
			
		||||
	Server           string
 | 
			
		||||
	NoTLS            bool
 | 
			
		||||
	SkipTLSVerify    bool
 | 
			
		||||
	SkipVersionCheck bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Message struct {
 | 
			
		||||
	Raw      *model.WebSocketEvent
 | 
			
		||||
	Post     *model.Post
 | 
			
		||||
	Team     string
 | 
			
		||||
	Channel  string
 | 
			
		||||
	Username string
 | 
			
		||||
	Text     string
 | 
			
		||||
	Type     string
 | 
			
		||||
	UserID   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//nolint:golint
 | 
			
		||||
type Team struct {
 | 
			
		||||
	Team         *model.Team
 | 
			
		||||
	Id           string
 | 
			
		||||
	Channels     []*model.Channel
 | 
			
		||||
	MoreChannels []*model.Channel
 | 
			
		||||
	Users        map[string]*model.User
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MMClient struct {
 | 
			
		||||
	sync.RWMutex
 | 
			
		||||
	*Credentials
 | 
			
		||||
 | 
			
		||||
	Team          *Team
 | 
			
		||||
	OtherTeams    []*Team
 | 
			
		||||
	Client        *model.Client4
 | 
			
		||||
	User          *model.User
 | 
			
		||||
	Users         map[string]*model.User
 | 
			
		||||
	MessageChan   chan *Message
 | 
			
		||||
	WsClient      *websocket.Conn
 | 
			
		||||
	WsQuit        bool
 | 
			
		||||
	WsAway        bool
 | 
			
		||||
	WsConnected   bool
 | 
			
		||||
	WsSequence    int64
 | 
			
		||||
	WsPingChan    chan *model.WebSocketResponse
 | 
			
		||||
	ServerVersion string
 | 
			
		||||
	OnWsConnect   func()
 | 
			
		||||
 | 
			
		||||
	logger     *logrus.Entry
 | 
			
		||||
	rootLogger *logrus.Logger
 | 
			
		||||
	lruCache   *lru.Cache
 | 
			
		||||
	allevents  bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New will instantiate a new Matterclient with the specified login details without connecting.
 | 
			
		||||
func New(login string, pass string, team string, server string) *MMClient {
 | 
			
		||||
	rootLogger := logrus.New()
 | 
			
		||||
	rootLogger.SetFormatter(&prefixed.TextFormatter{
 | 
			
		||||
		PrefixPadding: 13,
 | 
			
		||||
		DisableColors: true,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	cred := &Credentials{
 | 
			
		||||
		Login:  login,
 | 
			
		||||
		Pass:   pass,
 | 
			
		||||
		Team:   team,
 | 
			
		||||
		Server: server,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cache, _ := lru.New(500)
 | 
			
		||||
	return &MMClient{
 | 
			
		||||
		Credentials: cred,
 | 
			
		||||
		MessageChan: make(chan *Message, 100),
 | 
			
		||||
		Users:       make(map[string]*model.User),
 | 
			
		||||
		rootLogger:  rootLogger,
 | 
			
		||||
		lruCache:    cache,
 | 
			
		||||
		logger:      rootLogger.WithFields(logrus.Fields{"prefix": "matterclient"}),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetDebugLog activates debugging logging on all Matterclient log output.
 | 
			
		||||
func (m *MMClient) SetDebugLog() {
 | 
			
		||||
	m.rootLogger.SetFormatter(&prefixed.TextFormatter{
 | 
			
		||||
		PrefixPadding:   13,
 | 
			
		||||
		DisableColors:   true,
 | 
			
		||||
		FullTimestamp:   false,
 | 
			
		||||
		ForceFormatting: true,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetLogLevel tries to parse the specified level and if successful sets
 | 
			
		||||
// the log level accordingly. Accepted levels are: 'debug', 'info', 'warn',
 | 
			
		||||
// 'error', 'fatal' and 'panic'.
 | 
			
		||||
func (m *MMClient) SetLogLevel(level string) {
 | 
			
		||||
	l, err := logrus.ParseLevel(level)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		m.logger.Warnf("Failed to parse specified log-level '%s': %#v", level, err)
 | 
			
		||||
	} else {
 | 
			
		||||
		m.rootLogger.SetLevel(l)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) EnableAllEvents() {
 | 
			
		||||
	m.allevents = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Login tries to connect the client with the loging details with which it was initialized.
 | 
			
		||||
func (m *MMClient) Login() error {
 | 
			
		||||
	// check if this is a first connect or a reconnection
 | 
			
		||||
	firstConnection := true
 | 
			
		||||
	if m.WsConnected {
 | 
			
		||||
		firstConnection = false
 | 
			
		||||
	}
 | 
			
		||||
	m.WsConnected = false
 | 
			
		||||
	if m.WsQuit {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	b := &backoff.Backoff{
 | 
			
		||||
		Min:    time.Second,
 | 
			
		||||
		Max:    5 * time.Minute,
 | 
			
		||||
		Jitter: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// do initialization setup
 | 
			
		||||
	if err := m.initClient(firstConnection, b); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := m.doLogin(firstConnection, b); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := m.initUser(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if m.Team == nil {
 | 
			
		||||
		validTeamNames := make([]string, len(m.OtherTeams))
 | 
			
		||||
		for i, t := range m.OtherTeams {
 | 
			
		||||
			validTeamNames[i] = t.Team.Name
 | 
			
		||||
		}
 | 
			
		||||
		return fmt.Errorf("Team '%s' not found in %v", m.Credentials.Team, validTeamNames)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.wsConnect()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Logout disconnects the client from the chat server.
 | 
			
		||||
func (m *MMClient) Logout() error {
 | 
			
		||||
	m.logger.Debugf("logout as %s (team: %s) on %s", m.Credentials.Login, m.Credentials.Team, m.Credentials.Server)
 | 
			
		||||
	m.WsQuit = true
 | 
			
		||||
	m.WsClient.Close()
 | 
			
		||||
	m.WsClient.UnderlyingConn().Close()
 | 
			
		||||
	if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
 | 
			
		||||
		m.logger.Debug("Not invalidating session in logout, credential is a token")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	_, resp := m.Client.Logout()
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WsReceiver implements the core loop that manages the connection to the chat server. In
 | 
			
		||||
// case of a disconnect it will try to reconnect. A call to this method is blocking until
 | 
			
		||||
// the 'WsQuite' field of the MMClient object is set to 'true'.
 | 
			
		||||
func (m *MMClient) WsReceiver() {
 | 
			
		||||
	for {
 | 
			
		||||
		var rawMsg json.RawMessage
 | 
			
		||||
		var err error
 | 
			
		||||
 | 
			
		||||
		if m.WsQuit {
 | 
			
		||||
			m.logger.Debug("exiting WsReceiver")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !m.WsConnected {
 | 
			
		||||
			time.Sleep(time.Millisecond * 100)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil {
 | 
			
		||||
			m.logger.Error("error:", err)
 | 
			
		||||
			// reconnect
 | 
			
		||||
			m.wsConnect()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var event model.WebSocketEvent
 | 
			
		||||
		if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
 | 
			
		||||
			m.logger.Debugf("WsReceiver event: %#v", event)
 | 
			
		||||
			msg := &Message{Raw: &event, Team: m.Credentials.Team}
 | 
			
		||||
			m.parseMessage(msg)
 | 
			
		||||
			// check if we didn't empty the message
 | 
			
		||||
			if msg.Text != "" {
 | 
			
		||||
				m.MessageChan <- msg
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			// if we have file attached but the message is empty, also send it
 | 
			
		||||
			if msg.Post != nil {
 | 
			
		||||
				if msg.Text != "" || len(msg.Post.FileIds) > 0 || msg.Post.Type == "slack_attachment" {
 | 
			
		||||
					m.MessageChan <- msg
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if m.allevents {
 | 
			
		||||
				m.MessageChan <- msg
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			switch msg.Raw.Event {
 | 
			
		||||
			case model.WEBSOCKET_EVENT_USER_ADDED,
 | 
			
		||||
				model.WEBSOCKET_EVENT_USER_REMOVED,
 | 
			
		||||
				model.WEBSOCKET_EVENT_CHANNEL_CREATED,
 | 
			
		||||
				model.WEBSOCKET_EVENT_CHANNEL_DELETED:
 | 
			
		||||
				m.MessageChan <- msg
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var response model.WebSocketResponse
 | 
			
		||||
		if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
 | 
			
		||||
			m.logger.Debugf("WsReceiver response: %#v", response)
 | 
			
		||||
			m.parseResponse(response)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StatusLoop implements a ping-cycle that ensures that the connection to the chat servers
 | 
			
		||||
// remains alive. In case of a disconnect it will try to reconnect. A call to this method
 | 
			
		||||
// is blocking until the 'WsQuite' field of the MMClient object is set to 'true'.
 | 
			
		||||
func (m *MMClient) StatusLoop() {
 | 
			
		||||
	retries := 0
 | 
			
		||||
	backoff := time.Second * 60
 | 
			
		||||
	if m.OnWsConnect != nil {
 | 
			
		||||
		m.OnWsConnect()
 | 
			
		||||
	}
 | 
			
		||||
	m.logger.Debug("StatusLoop:", m.OnWsConnect != nil)
 | 
			
		||||
	for {
 | 
			
		||||
		if m.WsQuit {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if m.WsConnected {
 | 
			
		||||
			if err := m.checkAlive(); err != nil {
 | 
			
		||||
				m.logger.Errorf("Connection is not alive: %#v", err)
 | 
			
		||||
			}
 | 
			
		||||
			select {
 | 
			
		||||
			case <-m.WsPingChan:
 | 
			
		||||
				m.logger.Debug("WS PONG received")
 | 
			
		||||
				backoff = time.Second * 60
 | 
			
		||||
			case <-time.After(time.Second * 5):
 | 
			
		||||
				if retries > 3 {
 | 
			
		||||
					m.logger.Debug("StatusLoop() timeout")
 | 
			
		||||
					m.Logout()
 | 
			
		||||
					m.WsQuit = false
 | 
			
		||||
					err := m.Login()
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						m.logger.Errorf("Login failed: %#v", err)
 | 
			
		||||
						break
 | 
			
		||||
					}
 | 
			
		||||
					if m.OnWsConnect != nil {
 | 
			
		||||
						m.OnWsConnect()
 | 
			
		||||
					}
 | 
			
		||||
					go m.WsReceiver()
 | 
			
		||||
				} else {
 | 
			
		||||
					retries++
 | 
			
		||||
					backoff = time.Second * 5
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		time.Sleep(backoff)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,207 +0,0 @@
 | 
			
		||||
package matterclient
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattermost/mattermost-server/v5/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) parseActionPost(rmsg *Message) {
 | 
			
		||||
	// add post to cache, if it already exists don't relay this again.
 | 
			
		||||
	// this should fix reposts
 | 
			
		||||
	if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok && rmsg.Raw.Event != model.WEBSOCKET_EVENT_POST_DELETED {
 | 
			
		||||
		m.logger.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string))
 | 
			
		||||
		rmsg.Text = ""
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string)))
 | 
			
		||||
	// we don't have the user, refresh the userlist
 | 
			
		||||
	if m.GetUser(data.UserId) == nil {
 | 
			
		||||
		m.logger.Infof("User '%v' is not known, ignoring message '%#v'",
 | 
			
		||||
			data.UserId, data)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	rmsg.Username = m.GetUserName(data.UserId)
 | 
			
		||||
	rmsg.Channel = m.GetChannelName(data.ChannelId)
 | 
			
		||||
	rmsg.UserID = data.UserId
 | 
			
		||||
	rmsg.Type = data.Type
 | 
			
		||||
	teamid, _ := rmsg.Raw.Data["team_id"].(string)
 | 
			
		||||
	// edit messsages have no team_id for some reason
 | 
			
		||||
	if teamid == "" {
 | 
			
		||||
		// we can find the team_id from the channelid
 | 
			
		||||
		teamid = m.GetChannelTeamId(data.ChannelId)
 | 
			
		||||
		rmsg.Raw.Data["team_id"] = teamid
 | 
			
		||||
	}
 | 
			
		||||
	if teamid != "" {
 | 
			
		||||
		rmsg.Team = m.GetTeamName(teamid)
 | 
			
		||||
	}
 | 
			
		||||
	// direct message
 | 
			
		||||
	if rmsg.Raw.Data["channel_type"] == "D" {
 | 
			
		||||
		rmsg.Channel = m.GetUser(data.UserId).Username
 | 
			
		||||
	}
 | 
			
		||||
	rmsg.Text = data.Message
 | 
			
		||||
	rmsg.Post = data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) parseMessage(rmsg *Message) {
 | 
			
		||||
	switch rmsg.Raw.Event {
 | 
			
		||||
	case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED:
 | 
			
		||||
		m.parseActionPost(rmsg)
 | 
			
		||||
	case "user_updated":
 | 
			
		||||
		user := rmsg.Raw.Data["user"].(map[string]interface{})
 | 
			
		||||
		if _, ok := user["id"].(string); ok {
 | 
			
		||||
			m.UpdateUser(user["id"].(string))
 | 
			
		||||
		}
 | 
			
		||||
	case "group_added":
 | 
			
		||||
		if err := m.UpdateChannels(); err != nil {
 | 
			
		||||
			m.logger.Errorf("failed to update channels: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
		/*
 | 
			
		||||
			case model.ACTION_USER_REMOVED:
 | 
			
		||||
				m.handleWsActionUserRemoved(&rmsg)
 | 
			
		||||
			case model.ACTION_USER_ADDED:
 | 
			
		||||
				m.handleWsActionUserAdded(&rmsg)
 | 
			
		||||
		*/
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) {
 | 
			
		||||
	if rmsg.Data != nil {
 | 
			
		||||
		// ping reply
 | 
			
		||||
		if rmsg.Data["text"].(string) == "pong" {
 | 
			
		||||
			m.WsPingChan <- &rmsg
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) DeleteMessage(postId string) error { //nolint:golint
 | 
			
		||||
	_, resp := m.Client.DeletePost(postId)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint
 | 
			
		||||
	post := &model.Post{Message: text, Id: postId}
 | 
			
		||||
	res, resp := m.Client.UpdatePost(postId, post)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return "", resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	return res.Id, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetFileLinks(filenames []string) []string {
 | 
			
		||||
	uriScheme := "https://"
 | 
			
		||||
	if m.NoTLS {
 | 
			
		||||
		uriScheme = "http://"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var output []string
 | 
			
		||||
	for _, f := range filenames {
 | 
			
		||||
		res, resp := m.Client.GetFileLink(f)
 | 
			
		||||
		if resp.Error != nil {
 | 
			
		||||
			// public links is probably disabled, create the link ourselves
 | 
			
		||||
			output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V4+"/files/"+f)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		output = append(output, res)
 | 
			
		||||
	}
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList { //nolint:golint
 | 
			
		||||
	res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "", true)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList { //nolint:golint
 | 
			
		||||
	res, resp := m.Client.GetPostsSince(channelId, time, true)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetPublicLink(filename string) string {
 | 
			
		||||
	res, resp := m.Client.GetFileLink(filename)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetPublicLinks(filenames []string) []string {
 | 
			
		||||
	var output []string
 | 
			
		||||
	for _, f := range filenames {
 | 
			
		||||
		res, resp := m.Client.GetFileLink(f)
 | 
			
		||||
		if resp.Error != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		output = append(output, res)
 | 
			
		||||
	}
 | 
			
		||||
	return output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) PostMessage(channelId string, text string, rootId string) (string, error) { //nolint:golint
 | 
			
		||||
	post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId}
 | 
			
		||||
	res, resp := m.Client.CreatePost(post)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return "", resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	return res.Id, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) PostMessageWithFiles(channelId string, text string, rootId string, fileIds []string) (string, error) { //nolint:golint
 | 
			
		||||
	post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId, FileIds: fileIds}
 | 
			
		||||
	res, resp := m.Client.CreatePost(post)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return "", resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	return res.Id, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) SearchPosts(query string) *model.PostList {
 | 
			
		||||
	res, resp := m.Client.SearchPosts(m.Team.Id, query, false)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendDirectMessage sends a direct message to specified user
 | 
			
		||||
func (m *MMClient) SendDirectMessage(toUserId string, msg string, rootId string) { //nolint:golint
 | 
			
		||||
	m.SendDirectMessageProps(toUserId, msg, rootId, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) SendDirectMessageProps(toUserId string, msg string, rootId string, props map[string]interface{}) { //nolint:golint
 | 
			
		||||
	m.logger.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
 | 
			
		||||
	// create DM channel (only happens on first message)
 | 
			
		||||
	_, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		m.logger.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
 | 
			
		||||
 | 
			
		||||
	// update our channels
 | 
			
		||||
	if err := m.UpdateChannels(); err != nil {
 | 
			
		||||
		m.logger.Errorf("failed to update channels: %#v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// build & send the message
 | 
			
		||||
	msg = strings.Replace(msg, "\r", "", -1)
 | 
			
		||||
	post := &model.Post{ChannelId: m.GetChannelId(channelName, m.Team.Id), Message: msg, RootId: rootId, Props: props}
 | 
			
		||||
	m.Client.CreatePost(post)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) { //nolint:golint
 | 
			
		||||
	f, resp := m.Client.UploadFile(data, channelId, filename)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return "", resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	return f.FileInfos[0].Id, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,165 +0,0 @@
 | 
			
		||||
package matterclient
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattermost/mattermost-server/v5/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetNickName(userId string) string { //nolint:golint
 | 
			
		||||
	user := m.GetUser(userId)
 | 
			
		||||
	if user != nil {
 | 
			
		||||
		return user.Nickname
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetStatus(userId string) string { //nolint:golint
 | 
			
		||||
	res, resp := m.Client.GetUserStatus(userId, "")
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	if res.Status == model.STATUS_AWAY {
 | 
			
		||||
		return "away"
 | 
			
		||||
	}
 | 
			
		||||
	if res.Status == model.STATUS_ONLINE {
 | 
			
		||||
		return "online"
 | 
			
		||||
	}
 | 
			
		||||
	return "offline"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetStatuses() map[string]string {
 | 
			
		||||
	var ids []string
 | 
			
		||||
	statuses := make(map[string]string)
 | 
			
		||||
	for id := range m.Users {
 | 
			
		||||
		ids = append(ids, id)
 | 
			
		||||
	}
 | 
			
		||||
	res, resp := m.Client.GetUsersStatusesByIds(ids)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return statuses
 | 
			
		||||
	}
 | 
			
		||||
	for _, status := range res {
 | 
			
		||||
		statuses[status.UserId] = "offline"
 | 
			
		||||
		if status.Status == model.STATUS_AWAY {
 | 
			
		||||
			statuses[status.UserId] = "away"
 | 
			
		||||
		}
 | 
			
		||||
		if status.Status == model.STATUS_ONLINE {
 | 
			
		||||
			statuses[status.UserId] = "online"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return statuses
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetTeamId() string { //nolint:golint
 | 
			
		||||
	return m.Team.Id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTeamName returns the name of the specified teamId
 | 
			
		||||
func (m *MMClient) GetTeamName(teamId string) string { //nolint:golint
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	for _, t := range m.OtherTeams {
 | 
			
		||||
		if t.Id == teamId {
 | 
			
		||||
			return t.Team.Name
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetUser(userId string) *model.User { //nolint:golint
 | 
			
		||||
	m.Lock()
 | 
			
		||||
	defer m.Unlock()
 | 
			
		||||
	_, ok := m.Users[userId]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		res, resp := m.Client.GetUser(userId, "")
 | 
			
		||||
		if resp.Error != nil {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		m.Users[userId] = res
 | 
			
		||||
	}
 | 
			
		||||
	return m.Users[userId]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetUserName(userId string) string { //nolint:golint
 | 
			
		||||
	user := m.GetUser(userId)
 | 
			
		||||
	if user != nil {
 | 
			
		||||
		return user.Username
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) GetUsers() map[string]*model.User {
 | 
			
		||||
	users := make(map[string]*model.User)
 | 
			
		||||
	m.RLock()
 | 
			
		||||
	defer m.RUnlock()
 | 
			
		||||
	for k, v := range m.Users {
 | 
			
		||||
		users[k] = v
 | 
			
		||||
	}
 | 
			
		||||
	return users
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) UpdateUsers() error {
 | 
			
		||||
	idx := 0
 | 
			
		||||
	max := 200
 | 
			
		||||
	mmusers, resp := m.Client.GetUsers(idx, max, "")
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return errors.New(resp.Error.DetailedError)
 | 
			
		||||
	}
 | 
			
		||||
	for len(mmusers) > 0 {
 | 
			
		||||
		m.Lock()
 | 
			
		||||
		for _, user := range mmusers {
 | 
			
		||||
			m.Users[user.Id] = user
 | 
			
		||||
		}
 | 
			
		||||
		m.Unlock()
 | 
			
		||||
		mmusers, resp = m.Client.GetUsers(idx, max, "")
 | 
			
		||||
		time.Sleep(time.Millisecond * 300)
 | 
			
		||||
		if resp.Error != nil {
 | 
			
		||||
			return errors.New(resp.Error.DetailedError)
 | 
			
		||||
		}
 | 
			
		||||
		idx++
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) UpdateUserNick(nick string) error {
 | 
			
		||||
	user := m.User
 | 
			
		||||
	user.Nickname = nick
 | 
			
		||||
	_, resp := m.Client.UpdateUser(user)
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) UsernamesInChannel(channelId string) []string { //nolint:golint
 | 
			
		||||
	res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "")
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		m.logger.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error)
 | 
			
		||||
		return []string{}
 | 
			
		||||
	}
 | 
			
		||||
	allusers := m.GetUsers()
 | 
			
		||||
	result := []string{}
 | 
			
		||||
	for _, member := range *res {
 | 
			
		||||
		result = append(result, allusers[member.UserId].Nickname)
 | 
			
		||||
	}
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) UpdateStatus(userId string, status string) error { //nolint:golint
 | 
			
		||||
	_, resp := m.Client.UpdateUserStatus(userId, &model.Status{Status: status})
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return resp.Error
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MMClient) UpdateUser(userId string) { //nolint:golint
 | 
			
		||||
	m.Lock()
 | 
			
		||||
	defer m.Unlock()
 | 
			
		||||
	res, resp := m.Client.GetUser(userId, "")
 | 
			
		||||
	if resp.Error != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	m.Users[userId] = res
 | 
			
		||||
}
 | 
			
		||||
@@ -5,7 +5,7 @@ RUN apk add \
 | 
			
		||||
    go \
 | 
			
		||||
    git \
 | 
			
		||||
  && cd /go/src/matterbridge \
 | 
			
		||||
  && go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
 | 
			
		||||
  && CGO_ENABLED=0 go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
 | 
			
		||||
 | 
			
		||||
FROM alpine
 | 
			
		||||
RUN apk --no-cache add \
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								vendor/filippo.io/edwards25519/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								vendor/filippo.io/edwards25519/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
# filippo.io/edwards25519
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
import "filippo.io/edwards25519"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This library implements the edwards25519 elliptic curve, exposing the necessary APIs to build a wide array of higher-level primitives.
 | 
			
		||||
Read the docs at [pkg.go.dev/filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519).
 | 
			
		||||
 | 
			
		||||
The code is originally derived from Adam Langley's internal implementation in the Go standard library, and includes George Tankersley's [performance improvements](https://golang.org/cl/71950). It was then further developed by Henry de Valence for use in ristretto255, and was finally [merged back into the Go standard library](https://golang.org/cl/276272) as of Go 1.17. It now tracks the upstream codebase and extends it with additional functionality.
 | 
			
		||||
 | 
			
		||||
Most users don't need this package, and should instead use `crypto/ed25519` for signatures, `golang.org/x/crypto/curve25519` for Diffie-Hellman, or `github.com/gtank/ristretto255` for prime order group logic. However, for anyone currently using a fork of `crypto/internal/edwards25519`/`crypto/ed25519/internal/edwards25519` or `github.com/agl/edwards25519`, this package should be a safer, faster, and more powerful alternative.
 | 
			
		||||
 | 
			
		||||
Since this package is meant to curb proliferation of edwards25519 implementations in the Go ecosystem, it welcomes requests for new APIs or reviewable performance improvements.
 | 
			
		||||
							
								
								
									
										20
									
								
								vendor/filippo.io/edwards25519/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								vendor/filippo.io/edwards25519/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
// Copyright (c) 2021 The Go Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
// Package edwards25519 implements group logic for the twisted Edwards curve
 | 
			
		||||
//
 | 
			
		||||
//	-x^2 + y^2 = 1 + -(121665/121666)*x^2*y^2
 | 
			
		||||
//
 | 
			
		||||
// This is better known as the Edwards curve equivalent to Curve25519, and is
 | 
			
		||||
// the curve used by the Ed25519 signature scheme.
 | 
			
		||||
//
 | 
			
		||||
// Most users don't need this package, and should instead use crypto/ed25519 for
 | 
			
		||||
// signatures, golang.org/x/crypto/curve25519 for Diffie-Hellman, or
 | 
			
		||||
// github.com/gtank/ristretto255 for prime order group logic.
 | 
			
		||||
//
 | 
			
		||||
// However, developers who do need to interact with low-level edwards25519
 | 
			
		||||
// operations can use this package, which is an extended version of
 | 
			
		||||
// crypto/internal/edwards25519 from the standard library repackaged as
 | 
			
		||||
// an importable module.
 | 
			
		||||
package edwards25519
 | 
			
		||||
							
								
								
									
										427
									
								
								vendor/filippo.io/edwards25519/edwards25519.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										427
									
								
								vendor/filippo.io/edwards25519/edwards25519.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,427 @@
 | 
			
		||||
// Copyright (c) 2017 The Go Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package edwards25519
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"filippo.io/edwards25519/field"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Point types.
 | 
			
		||||
 | 
			
		||||
type projP1xP1 struct {
 | 
			
		||||
	X, Y, Z, T field.Element
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type projP2 struct {
 | 
			
		||||
	X, Y, Z field.Element
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Point represents a point on the edwards25519 curve.
 | 
			
		||||
//
 | 
			
		||||
// This type works similarly to math/big.Int, and all arguments and receivers
 | 
			
		||||
// are allowed to alias.
 | 
			
		||||
//
 | 
			
		||||
// The zero value is NOT valid, and it may be used only as a receiver.
 | 
			
		||||
type Point struct {
 | 
			
		||||
	// Make the type not comparable (i.e. used with == or as a map key), as
 | 
			
		||||
	// equivalent points can be represented by different Go values.
 | 
			
		||||
	_ incomparable
 | 
			
		||||
 | 
			
		||||
	// The point is internally represented in extended coordinates (X, Y, Z, T)
 | 
			
		||||
	// where x = X/Z, y = Y/Z, and xy = T/Z per https://eprint.iacr.org/2008/522.
 | 
			
		||||
	x, y, z, t field.Element
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type incomparable [0]func()
 | 
			
		||||
 | 
			
		||||
func checkInitialized(points ...*Point) {
 | 
			
		||||
	for _, p := range points {
 | 
			
		||||
		if p.x == (field.Element{}) && p.y == (field.Element{}) {
 | 
			
		||||
			panic("edwards25519: use of uninitialized Point")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type projCached struct {
 | 
			
		||||
	YplusX, YminusX, Z, T2d field.Element
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type affineCached struct {
 | 
			
		||||
	YplusX, YminusX, T2d field.Element
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Constructors.
 | 
			
		||||
 | 
			
		||||
func (v *projP2) Zero() *projP2 {
 | 
			
		||||
	v.X.Zero()
 | 
			
		||||
	v.Y.One()
 | 
			
		||||
	v.Z.One()
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// identity is the point at infinity.
 | 
			
		||||
var identity, _ = new(Point).SetBytes([]byte{
 | 
			
		||||
	1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
 | 
			
		||||
	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
 | 
			
		||||
 | 
			
		||||
// NewIdentityPoint returns a new Point set to the identity.
 | 
			
		||||
func NewIdentityPoint() *Point {
 | 
			
		||||
	return new(Point).Set(identity)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// generator is the canonical curve basepoint. See TestGenerator for the
 | 
			
		||||
// correspondence of this encoding with the values in RFC 8032.
 | 
			
		||||
var generator, _ = new(Point).SetBytes([]byte{
 | 
			
		||||
	0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
 | 
			
		||||
	0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
 | 
			
		||||
	0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
 | 
			
		||||
	0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66})
 | 
			
		||||
 | 
			
		||||
// NewGeneratorPoint returns a new Point set to the canonical generator.
 | 
			
		||||
func NewGeneratorPoint() *Point {
 | 
			
		||||
	return new(Point).Set(generator)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *projCached) Zero() *projCached {
 | 
			
		||||
	v.YplusX.One()
 | 
			
		||||
	v.YminusX.One()
 | 
			
		||||
	v.Z.One()
 | 
			
		||||
	v.T2d.Zero()
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *affineCached) Zero() *affineCached {
 | 
			
		||||
	v.YplusX.One()
 | 
			
		||||
	v.YminusX.One()
 | 
			
		||||
	v.T2d.Zero()
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Assignments.
 | 
			
		||||
 | 
			
		||||
// Set sets v = u, and returns v.
 | 
			
		||||
func (v *Point) Set(u *Point) *Point {
 | 
			
		||||
	*v = *u
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Encoding.
 | 
			
		||||
 | 
			
		||||
// Bytes returns the canonical 32-byte encoding of v, according to RFC 8032,
 | 
			
		||||
// Section 5.1.2.
 | 
			
		||||
func (v *Point) Bytes() []byte {
 | 
			
		||||
	// This function is outlined to make the allocations inline in the caller
 | 
			
		||||
	// rather than happen on the heap.
 | 
			
		||||
	var buf [32]byte
 | 
			
		||||
	return v.bytes(&buf)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *Point) bytes(buf *[32]byte) []byte {
 | 
			
		||||
	checkInitialized(v)
 | 
			
		||||
 | 
			
		||||
	var zInv, x, y field.Element
 | 
			
		||||
	zInv.Invert(&v.z)       // zInv = 1 / Z
 | 
			
		||||
	x.Multiply(&v.x, &zInv) // x = X / Z
 | 
			
		||||
	y.Multiply(&v.y, &zInv) // y = Y / Z
 | 
			
		||||
 | 
			
		||||
	out := copyFieldElement(buf, &y)
 | 
			
		||||
	out[31] |= byte(x.IsNegative() << 7)
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var feOne = new(field.Element).One()
 | 
			
		||||
 | 
			
		||||
// SetBytes sets v = x, where x is a 32-byte encoding of v. If x does not
 | 
			
		||||
// represent a valid point on the curve, SetBytes returns nil and an error and
 | 
			
		||||
// the receiver is unchanged. Otherwise, SetBytes returns v.
 | 
			
		||||
//
 | 
			
		||||
// Note that SetBytes accepts all non-canonical encodings of valid points.
 | 
			
		||||
// That is, it follows decoding rules that match most implementations in
 | 
			
		||||
// the ecosystem rather than RFC 8032.
 | 
			
		||||
func (v *Point) SetBytes(x []byte) (*Point, error) {
 | 
			
		||||
	// Specifically, the non-canonical encodings that are accepted are
 | 
			
		||||
	//   1) the ones where the field element is not reduced (see the
 | 
			
		||||
	//      (*field.Element).SetBytes docs) and
 | 
			
		||||
	//   2) the ones where the x-coordinate is zero and the sign bit is set.
 | 
			
		||||
	//
 | 
			
		||||
	// Read more at https://hdevalence.ca/blog/2020-10-04-its-25519am,
 | 
			
		||||
	// specifically the "Canonical A, R" section.
 | 
			
		||||
 | 
			
		||||
	y, err := new(field.Element).SetBytes(x)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errors.New("edwards25519: invalid point encoding length")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// -x² + y² = 1 + dx²y²
 | 
			
		||||
	// x² + dx²y² = x²(dy² + 1) = y² - 1
 | 
			
		||||
	// x² = (y² - 1) / (dy² + 1)
 | 
			
		||||
 | 
			
		||||
	// u = y² - 1
 | 
			
		||||
	y2 := new(field.Element).Square(y)
 | 
			
		||||
	u := new(field.Element).Subtract(y2, feOne)
 | 
			
		||||
 | 
			
		||||
	// v = dy² + 1
 | 
			
		||||
	vv := new(field.Element).Multiply(y2, d)
 | 
			
		||||
	vv = vv.Add(vv, feOne)
 | 
			
		||||
 | 
			
		||||
	// x = +√(u/v)
 | 
			
		||||
	xx, wasSquare := new(field.Element).SqrtRatio(u, vv)
 | 
			
		||||
	if wasSquare == 0 {
 | 
			
		||||
		return nil, errors.New("edwards25519: invalid point encoding")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Select the negative square root if the sign bit is set.
 | 
			
		||||
	xxNeg := new(field.Element).Negate(xx)
 | 
			
		||||
	xx = xx.Select(xxNeg, xx, int(x[31]>>7))
 | 
			
		||||
 | 
			
		||||
	v.x.Set(xx)
 | 
			
		||||
	v.y.Set(y)
 | 
			
		||||
	v.z.One()
 | 
			
		||||
	v.t.Multiply(xx, y) // xy = T / Z
 | 
			
		||||
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func copyFieldElement(buf *[32]byte, v *field.Element) []byte {
 | 
			
		||||
	copy(buf[:], v.Bytes())
 | 
			
		||||
	return buf[:]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Conversions.
 | 
			
		||||
 | 
			
		||||
func (v *projP2) FromP1xP1(p *projP1xP1) *projP2 {
 | 
			
		||||
	v.X.Multiply(&p.X, &p.T)
 | 
			
		||||
	v.Y.Multiply(&p.Y, &p.Z)
 | 
			
		||||
	v.Z.Multiply(&p.Z, &p.T)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *projP2) FromP3(p *Point) *projP2 {
 | 
			
		||||
	v.X.Set(&p.x)
 | 
			
		||||
	v.Y.Set(&p.y)
 | 
			
		||||
	v.Z.Set(&p.z)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *Point) fromP1xP1(p *projP1xP1) *Point {
 | 
			
		||||
	v.x.Multiply(&p.X, &p.T)
 | 
			
		||||
	v.y.Multiply(&p.Y, &p.Z)
 | 
			
		||||
	v.z.Multiply(&p.Z, &p.T)
 | 
			
		||||
	v.t.Multiply(&p.X, &p.Y)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *Point) fromP2(p *projP2) *Point {
 | 
			
		||||
	v.x.Multiply(&p.X, &p.Z)
 | 
			
		||||
	v.y.Multiply(&p.Y, &p.Z)
 | 
			
		||||
	v.z.Square(&p.Z)
 | 
			
		||||
	v.t.Multiply(&p.X, &p.Y)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// d is a constant in the curve equation.
 | 
			
		||||
var d, _ = new(field.Element).SetBytes([]byte{
 | 
			
		||||
	0xa3, 0x78, 0x59, 0x13, 0xca, 0x4d, 0xeb, 0x75,
 | 
			
		||||
	0xab, 0xd8, 0x41, 0x41, 0x4d, 0x0a, 0x70, 0x00,
 | 
			
		||||
	0x98, 0xe8, 0x79, 0x77, 0x79, 0x40, 0xc7, 0x8c,
 | 
			
		||||
	0x73, 0xfe, 0x6f, 0x2b, 0xee, 0x6c, 0x03, 0x52})
 | 
			
		||||
var d2 = new(field.Element).Add(d, d)
 | 
			
		||||
 | 
			
		||||
func (v *projCached) FromP3(p *Point) *projCached {
 | 
			
		||||
	v.YplusX.Add(&p.y, &p.x)
 | 
			
		||||
	v.YminusX.Subtract(&p.y, &p.x)
 | 
			
		||||
	v.Z.Set(&p.z)
 | 
			
		||||
	v.T2d.Multiply(&p.t, d2)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *affineCached) FromP3(p *Point) *affineCached {
 | 
			
		||||
	v.YplusX.Add(&p.y, &p.x)
 | 
			
		||||
	v.YminusX.Subtract(&p.y, &p.x)
 | 
			
		||||
	v.T2d.Multiply(&p.t, d2)
 | 
			
		||||
 | 
			
		||||
	var invZ field.Element
 | 
			
		||||
	invZ.Invert(&p.z)
 | 
			
		||||
	v.YplusX.Multiply(&v.YplusX, &invZ)
 | 
			
		||||
	v.YminusX.Multiply(&v.YminusX, &invZ)
 | 
			
		||||
	v.T2d.Multiply(&v.T2d, &invZ)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// (Re)addition and subtraction.
 | 
			
		||||
 | 
			
		||||
// Add sets v = p + q, and returns v.
 | 
			
		||||
func (v *Point) Add(p, q *Point) *Point {
 | 
			
		||||
	checkInitialized(p, q)
 | 
			
		||||
	qCached := new(projCached).FromP3(q)
 | 
			
		||||
	result := new(projP1xP1).Add(p, qCached)
 | 
			
		||||
	return v.fromP1xP1(result)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Subtract sets v = p - q, and returns v.
 | 
			
		||||
func (v *Point) Subtract(p, q *Point) *Point {
 | 
			
		||||
	checkInitialized(p, q)
 | 
			
		||||
	qCached := new(projCached).FromP3(q)
 | 
			
		||||
	result := new(projP1xP1).Sub(p, qCached)
 | 
			
		||||
	return v.fromP1xP1(result)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *projP1xP1) Add(p *Point, q *projCached) *projP1xP1 {
 | 
			
		||||
	var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element
 | 
			
		||||
 | 
			
		||||
	YplusX.Add(&p.y, &p.x)
 | 
			
		||||
	YminusX.Subtract(&p.y, &p.x)
 | 
			
		||||
 | 
			
		||||
	PP.Multiply(&YplusX, &q.YplusX)
 | 
			
		||||
	MM.Multiply(&YminusX, &q.YminusX)
 | 
			
		||||
	TT2d.Multiply(&p.t, &q.T2d)
 | 
			
		||||
	ZZ2.Multiply(&p.z, &q.Z)
 | 
			
		||||
 | 
			
		||||
	ZZ2.Add(&ZZ2, &ZZ2)
 | 
			
		||||
 | 
			
		||||
	v.X.Subtract(&PP, &MM)
 | 
			
		||||
	v.Y.Add(&PP, &MM)
 | 
			
		||||
	v.Z.Add(&ZZ2, &TT2d)
 | 
			
		||||
	v.T.Subtract(&ZZ2, &TT2d)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *projP1xP1) Sub(p *Point, q *projCached) *projP1xP1 {
 | 
			
		||||
	var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element
 | 
			
		||||
 | 
			
		||||
	YplusX.Add(&p.y, &p.x)
 | 
			
		||||
	YminusX.Subtract(&p.y, &p.x)
 | 
			
		||||
 | 
			
		||||
	PP.Multiply(&YplusX, &q.YminusX) // flipped sign
 | 
			
		||||
	MM.Multiply(&YminusX, &q.YplusX) // flipped sign
 | 
			
		||||
	TT2d.Multiply(&p.t, &q.T2d)
 | 
			
		||||
	ZZ2.Multiply(&p.z, &q.Z)
 | 
			
		||||
 | 
			
		||||
	ZZ2.Add(&ZZ2, &ZZ2)
 | 
			
		||||
 | 
			
		||||
	v.X.Subtract(&PP, &MM)
 | 
			
		||||
	v.Y.Add(&PP, &MM)
 | 
			
		||||
	v.Z.Subtract(&ZZ2, &TT2d) // flipped sign
 | 
			
		||||
	v.T.Add(&ZZ2, &TT2d)      // flipped sign
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *projP1xP1) AddAffine(p *Point, q *affineCached) *projP1xP1 {
 | 
			
		||||
	var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element
 | 
			
		||||
 | 
			
		||||
	YplusX.Add(&p.y, &p.x)
 | 
			
		||||
	YminusX.Subtract(&p.y, &p.x)
 | 
			
		||||
 | 
			
		||||
	PP.Multiply(&YplusX, &q.YplusX)
 | 
			
		||||
	MM.Multiply(&YminusX, &q.YminusX)
 | 
			
		||||
	TT2d.Multiply(&p.t, &q.T2d)
 | 
			
		||||
 | 
			
		||||
	Z2.Add(&p.z, &p.z)
 | 
			
		||||
 | 
			
		||||
	v.X.Subtract(&PP, &MM)
 | 
			
		||||
	v.Y.Add(&PP, &MM)
 | 
			
		||||
	v.Z.Add(&Z2, &TT2d)
 | 
			
		||||
	v.T.Subtract(&Z2, &TT2d)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *projP1xP1) SubAffine(p *Point, q *affineCached) *projP1xP1 {
 | 
			
		||||
	var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element
 | 
			
		||||
 | 
			
		||||
	YplusX.Add(&p.y, &p.x)
 | 
			
		||||
	YminusX.Subtract(&p.y, &p.x)
 | 
			
		||||
 | 
			
		||||
	PP.Multiply(&YplusX, &q.YminusX) // flipped sign
 | 
			
		||||
	MM.Multiply(&YminusX, &q.YplusX) // flipped sign
 | 
			
		||||
	TT2d.Multiply(&p.t, &q.T2d)
 | 
			
		||||
 | 
			
		||||
	Z2.Add(&p.z, &p.z)
 | 
			
		||||
 | 
			
		||||
	v.X.Subtract(&PP, &MM)
 | 
			
		||||
	v.Y.Add(&PP, &MM)
 | 
			
		||||
	v.Z.Subtract(&Z2, &TT2d) // flipped sign
 | 
			
		||||
	v.T.Add(&Z2, &TT2d)      // flipped sign
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Doubling.
 | 
			
		||||
 | 
			
		||||
func (v *projP1xP1) Double(p *projP2) *projP1xP1 {
 | 
			
		||||
	var XX, YY, ZZ2, XplusYsq field.Element
 | 
			
		||||
 | 
			
		||||
	XX.Square(&p.X)
 | 
			
		||||
	YY.Square(&p.Y)
 | 
			
		||||
	ZZ2.Square(&p.Z)
 | 
			
		||||
	ZZ2.Add(&ZZ2, &ZZ2)
 | 
			
		||||
	XplusYsq.Add(&p.X, &p.Y)
 | 
			
		||||
	XplusYsq.Square(&XplusYsq)
 | 
			
		||||
 | 
			
		||||
	v.Y.Add(&YY, &XX)
 | 
			
		||||
	v.Z.Subtract(&YY, &XX)
 | 
			
		||||
 | 
			
		||||
	v.X.Subtract(&XplusYsq, &v.Y)
 | 
			
		||||
	v.T.Subtract(&ZZ2, &v.Z)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Negation.
 | 
			
		||||
 | 
			
		||||
// Negate sets v = -p, and returns v.
 | 
			
		||||
func (v *Point) Negate(p *Point) *Point {
 | 
			
		||||
	checkInitialized(p)
 | 
			
		||||
	v.x.Negate(&p.x)
 | 
			
		||||
	v.y.Set(&p.y)
 | 
			
		||||
	v.z.Set(&p.z)
 | 
			
		||||
	v.t.Negate(&p.t)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Equal returns 1 if v is equivalent to u, and 0 otherwise.
 | 
			
		||||
func (v *Point) Equal(u *Point) int {
 | 
			
		||||
	checkInitialized(v, u)
 | 
			
		||||
 | 
			
		||||
	var t1, t2, t3, t4 field.Element
 | 
			
		||||
	t1.Multiply(&v.x, &u.z)
 | 
			
		||||
	t2.Multiply(&u.x, &v.z)
 | 
			
		||||
	t3.Multiply(&v.y, &u.z)
 | 
			
		||||
	t4.Multiply(&u.y, &v.z)
 | 
			
		||||
 | 
			
		||||
	return t1.Equal(&t2) & t3.Equal(&t4)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Constant-time operations
 | 
			
		||||
 | 
			
		||||
// Select sets v to a if cond == 1 and to b if cond == 0.
 | 
			
		||||
func (v *projCached) Select(a, b *projCached, cond int) *projCached {
 | 
			
		||||
	v.YplusX.Select(&a.YplusX, &b.YplusX, cond)
 | 
			
		||||
	v.YminusX.Select(&a.YminusX, &b.YminusX, cond)
 | 
			
		||||
	v.Z.Select(&a.Z, &b.Z, cond)
 | 
			
		||||
	v.T2d.Select(&a.T2d, &b.T2d, cond)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Select sets v to a if cond == 1 and to b if cond == 0.
 | 
			
		||||
func (v *affineCached) Select(a, b *affineCached, cond int) *affineCached {
 | 
			
		||||
	v.YplusX.Select(&a.YplusX, &b.YplusX, cond)
 | 
			
		||||
	v.YminusX.Select(&a.YminusX, &b.YminusX, cond)
 | 
			
		||||
	v.T2d.Select(&a.T2d, &b.T2d, cond)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0.
 | 
			
		||||
func (v *projCached) CondNeg(cond int) *projCached {
 | 
			
		||||
	v.YplusX.Swap(&v.YminusX, cond)
 | 
			
		||||
	v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0.
 | 
			
		||||
func (v *affineCached) CondNeg(cond int) *affineCached {
 | 
			
		||||
	v.YplusX.Swap(&v.YminusX, cond)
 | 
			
		||||
	v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										349
									
								
								vendor/filippo.io/edwards25519/extra.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										349
									
								
								vendor/filippo.io/edwards25519/extra.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,349 @@
 | 
			
		||||
// Copyright (c) 2021 The Go Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package edwards25519
 | 
			
		||||
 | 
			
		||||
// This file contains additional functionality that is not included in the
 | 
			
		||||
// upstream crypto/internal/edwards25519 package.
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"filippo.io/edwards25519/field"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ExtendedCoordinates returns v in extended coordinates (X:Y:Z:T) where
 | 
			
		||||
// x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522.
 | 
			
		||||
func (v *Point) ExtendedCoordinates() (X, Y, Z, T *field.Element) {
 | 
			
		||||
	// This function is outlined to make the allocations inline in the caller
 | 
			
		||||
	// rather than happen on the heap. Don't change the style without making
 | 
			
		||||
	// sure it doesn't increase the inliner cost.
 | 
			
		||||
	var e [4]field.Element
 | 
			
		||||
	X, Y, Z, T = v.extendedCoordinates(&e)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *Point) extendedCoordinates(e *[4]field.Element) (X, Y, Z, T *field.Element) {
 | 
			
		||||
	checkInitialized(v)
 | 
			
		||||
	X = e[0].Set(&v.x)
 | 
			
		||||
	Y = e[1].Set(&v.y)
 | 
			
		||||
	Z = e[2].Set(&v.z)
 | 
			
		||||
	T = e[3].Set(&v.t)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetExtendedCoordinates sets v = (X:Y:Z:T) in extended coordinates where
 | 
			
		||||
// x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522.
 | 
			
		||||
//
 | 
			
		||||
// If the coordinates are invalid or don't represent a valid point on the curve,
 | 
			
		||||
// SetExtendedCoordinates returns nil and an error and the receiver is
 | 
			
		||||
// unchanged. Otherwise, SetExtendedCoordinates returns v.
 | 
			
		||||
func (v *Point) SetExtendedCoordinates(X, Y, Z, T *field.Element) (*Point, error) {
 | 
			
		||||
	if !isOnCurve(X, Y, Z, T) {
 | 
			
		||||
		return nil, errors.New("edwards25519: invalid point coordinates")
 | 
			
		||||
	}
 | 
			
		||||
	v.x.Set(X)
 | 
			
		||||
	v.y.Set(Y)
 | 
			
		||||
	v.z.Set(Z)
 | 
			
		||||
	v.t.Set(T)
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isOnCurve(X, Y, Z, T *field.Element) bool {
 | 
			
		||||
	var lhs, rhs field.Element
 | 
			
		||||
	XX := new(field.Element).Square(X)
 | 
			
		||||
	YY := new(field.Element).Square(Y)
 | 
			
		||||
	ZZ := new(field.Element).Square(Z)
 | 
			
		||||
	TT := new(field.Element).Square(T)
 | 
			
		||||
	// -x² + y² = 1 + dx²y²
 | 
			
		||||
	// -(X/Z)² + (Y/Z)² = 1 + d(T/Z)²
 | 
			
		||||
	// -X² + Y² = Z² + dT²
 | 
			
		||||
	lhs.Subtract(YY, XX)
 | 
			
		||||
	rhs.Multiply(d, TT).Add(&rhs, ZZ)
 | 
			
		||||
	if lhs.Equal(&rhs) != 1 {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	// xy = T/Z
 | 
			
		||||
	// XY/Z² = T/Z
 | 
			
		||||
	// XY = TZ
 | 
			
		||||
	lhs.Multiply(X, Y)
 | 
			
		||||
	rhs.Multiply(T, Z)
 | 
			
		||||
	return lhs.Equal(&rhs) == 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BytesMontgomery converts v to a point on the birationally-equivalent
 | 
			
		||||
// Curve25519 Montgomery curve, and returns its canonical 32 bytes encoding
 | 
			
		||||
// according to RFC 7748.
 | 
			
		||||
//
 | 
			
		||||
// Note that BytesMontgomery only encodes the u-coordinate, so v and -v encode
 | 
			
		||||
// to the same value. If v is the identity point, BytesMontgomery returns 32
 | 
			
		||||
// zero bytes, analogously to the X25519 function.
 | 
			
		||||
//
 | 
			
		||||
// The lack of an inverse operation (such as SetMontgomeryBytes) is deliberate:
 | 
			
		||||
// while every valid edwards25519 point has a unique u-coordinate Montgomery
 | 
			
		||||
// encoding, X25519 accepts inputs on the quadratic twist, which don't correspond
 | 
			
		||||
// to any edwards25519 point, and every other X25519 input corresponds to two
 | 
			
		||||
// edwards25519 points.
 | 
			
		||||
func (v *Point) BytesMontgomery() []byte {
 | 
			
		||||
	// This function is outlined to make the allocations inline in the caller
 | 
			
		||||
	// rather than happen on the heap.
 | 
			
		||||
	var buf [32]byte
 | 
			
		||||
	return v.bytesMontgomery(&buf)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *Point) bytesMontgomery(buf *[32]byte) []byte {
 | 
			
		||||
	checkInitialized(v)
 | 
			
		||||
 | 
			
		||||
	// RFC 7748, Section 4.1 provides the bilinear map to calculate the
 | 
			
		||||
	// Montgomery u-coordinate
 | 
			
		||||
	//
 | 
			
		||||
	//              u = (1 + y) / (1 - y)
 | 
			
		||||
	//
 | 
			
		||||
	// where y = Y / Z.
 | 
			
		||||
 | 
			
		||||
	var y, recip, u field.Element
 | 
			
		||||
 | 
			
		||||
	y.Multiply(&v.y, y.Invert(&v.z))        // y = Y / Z
 | 
			
		||||
	recip.Invert(recip.Subtract(feOne, &y)) // r = 1/(1 - y)
 | 
			
		||||
	u.Multiply(u.Add(feOne, &y), &recip)    // u = (1 + y)*r
 | 
			
		||||
 | 
			
		||||
	return copyFieldElement(buf, &u)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MultByCofactor sets v = 8 * p, and returns v.
 | 
			
		||||
func (v *Point) MultByCofactor(p *Point) *Point {
 | 
			
		||||
	checkInitialized(p)
 | 
			
		||||
	result := projP1xP1{}
 | 
			
		||||
	pp := (&projP2{}).FromP3(p)
 | 
			
		||||
	result.Double(pp)
 | 
			
		||||
	pp.FromP1xP1(&result)
 | 
			
		||||
	result.Double(pp)
 | 
			
		||||
	pp.FromP1xP1(&result)
 | 
			
		||||
	result.Double(pp)
 | 
			
		||||
	return v.fromP1xP1(&result)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Given k > 0, set s = s**(2*i).
 | 
			
		||||
func (s *Scalar) pow2k(k int) {
 | 
			
		||||
	for i := 0; i < k; i++ {
 | 
			
		||||
		s.Multiply(s, s)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Invert sets s to the inverse of a nonzero scalar v, and returns s.
 | 
			
		||||
//
 | 
			
		||||
// If t is zero, Invert returns zero.
 | 
			
		||||
func (s *Scalar) Invert(t *Scalar) *Scalar {
 | 
			
		||||
	// Uses a hardcoded sliding window of width 4.
 | 
			
		||||
	var table [8]Scalar
 | 
			
		||||
	var tt Scalar
 | 
			
		||||
	tt.Multiply(t, t)
 | 
			
		||||
	table[0] = *t
 | 
			
		||||
	for i := 0; i < 7; i++ {
 | 
			
		||||
		table[i+1].Multiply(&table[i], &tt)
 | 
			
		||||
	}
 | 
			
		||||
	// Now table = [t**1, t**3, t**5, t**7, t**9, t**11, t**13, t**15]
 | 
			
		||||
	// so t**k = t[k/2] for odd k
 | 
			
		||||
 | 
			
		||||
	// To compute the sliding window digits, use the following Sage script:
 | 
			
		||||
 | 
			
		||||
	// sage: import itertools
 | 
			
		||||
	// sage: def sliding_window(w,k):
 | 
			
		||||
	// ....:     digits = []
 | 
			
		||||
	// ....:     while k > 0:
 | 
			
		||||
	// ....:         if k % 2 == 1:
 | 
			
		||||
	// ....:             kmod = k % (2**w)
 | 
			
		||||
	// ....:             digits.append(kmod)
 | 
			
		||||
	// ....:             k = k - kmod
 | 
			
		||||
	// ....:         else:
 | 
			
		||||
	// ....:             digits.append(0)
 | 
			
		||||
	// ....:         k = k // 2
 | 
			
		||||
	// ....:     return digits
 | 
			
		||||
 | 
			
		||||
	// Now we can compute s roughly as follows:
 | 
			
		||||
 | 
			
		||||
	// sage: s = 1
 | 
			
		||||
	// sage: for coeff in reversed(sliding_window(4,l-2)):
 | 
			
		||||
	// ....:     s = s*s
 | 
			
		||||
	// ....:     if coeff > 0 :
 | 
			
		||||
	// ....:         s = s*t**coeff
 | 
			
		||||
 | 
			
		||||
	// This works on one bit at a time, with many runs of zeros.
 | 
			
		||||
	// The digits can be collapsed into [(count, coeff)] as follows:
 | 
			
		||||
 | 
			
		||||
	// sage: [(len(list(group)),d) for d,group in itertools.groupby(sliding_window(4,l-2))]
 | 
			
		||||
 | 
			
		||||
	// Entries of the form (k, 0) turn into pow2k(k)
 | 
			
		||||
	// Entries of the form (1, coeff) turn into a squaring and then a table lookup.
 | 
			
		||||
	// We can fold the squaring into the previous pow2k(k) as pow2k(k+1).
 | 
			
		||||
 | 
			
		||||
	*s = table[1/2]
 | 
			
		||||
	s.pow2k(127 + 1)
 | 
			
		||||
	s.Multiply(s, &table[1/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[9/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[11/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[13/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[15/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[7/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[15/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[5/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[1/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[15/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[15/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[7/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[3/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[11/2])
 | 
			
		||||
	s.pow2k(5 + 1)
 | 
			
		||||
	s.Multiply(s, &table[11/2])
 | 
			
		||||
	s.pow2k(9 + 1)
 | 
			
		||||
	s.Multiply(s, &table[9/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[3/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[3/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[3/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[9/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[7/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[3/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[13/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[7/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[9/2])
 | 
			
		||||
	s.pow2k(3 + 1)
 | 
			
		||||
	s.Multiply(s, &table[15/2])
 | 
			
		||||
	s.pow2k(4 + 1)
 | 
			
		||||
	s.Multiply(s, &table[11/2])
 | 
			
		||||
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v.
 | 
			
		||||
//
 | 
			
		||||
// Execution time depends only on the lengths of the two slices, which must match.
 | 
			
		||||
func (v *Point) MultiScalarMult(scalars []*Scalar, points []*Point) *Point {
 | 
			
		||||
	if len(scalars) != len(points) {
 | 
			
		||||
		panic("edwards25519: called MultiScalarMult with different size inputs")
 | 
			
		||||
	}
 | 
			
		||||
	checkInitialized(points...)
 | 
			
		||||
 | 
			
		||||
	// Proceed as in the single-base case, but share doublings
 | 
			
		||||
	// between each point in the multiscalar equation.
 | 
			
		||||
 | 
			
		||||
	// Build lookup tables for each point
 | 
			
		||||
	tables := make([]projLookupTable, len(points))
 | 
			
		||||
	for i := range tables {
 | 
			
		||||
		tables[i].FromP3(points[i])
 | 
			
		||||
	}
 | 
			
		||||
	// Compute signed radix-16 digits for each scalar
 | 
			
		||||
	digits := make([][64]int8, len(scalars))
 | 
			
		||||
	for i := range digits {
 | 
			
		||||
		digits[i] = scalars[i].signedRadix16()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Unwrap first loop iteration to save computing 16*identity
 | 
			
		||||
	multiple := &projCached{}
 | 
			
		||||
	tmp1 := &projP1xP1{}
 | 
			
		||||
	tmp2 := &projP2{}
 | 
			
		||||
	// Lookup-and-add the appropriate multiple of each input point
 | 
			
		||||
	for j := range tables {
 | 
			
		||||
		tables[j].SelectInto(multiple, digits[j][63])
 | 
			
		||||
		tmp1.Add(v, multiple) // tmp1 = v + x_(j,63)*Q in P1xP1 coords
 | 
			
		||||
		v.fromP1xP1(tmp1)     // update v
 | 
			
		||||
	}
 | 
			
		||||
	tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration
 | 
			
		||||
	for i := 62; i >= 0; i-- {
 | 
			
		||||
		tmp1.Double(tmp2)    // tmp1 =  2*(prev) in P1xP1 coords
 | 
			
		||||
		tmp2.FromP1xP1(tmp1) // tmp2 =  2*(prev) in P2 coords
 | 
			
		||||
		tmp1.Double(tmp2)    // tmp1 =  4*(prev) in P1xP1 coords
 | 
			
		||||
		tmp2.FromP1xP1(tmp1) // tmp2 =  4*(prev) in P2 coords
 | 
			
		||||
		tmp1.Double(tmp2)    // tmp1 =  8*(prev) in P1xP1 coords
 | 
			
		||||
		tmp2.FromP1xP1(tmp1) // tmp2 =  8*(prev) in P2 coords
 | 
			
		||||
		tmp1.Double(tmp2)    // tmp1 = 16*(prev) in P1xP1 coords
 | 
			
		||||
		v.fromP1xP1(tmp1)    //    v = 16*(prev) in P3 coords
 | 
			
		||||
		// Lookup-and-add the appropriate multiple of each input point
 | 
			
		||||
		for j := range tables {
 | 
			
		||||
			tables[j].SelectInto(multiple, digits[j][i])
 | 
			
		||||
			tmp1.Add(v, multiple) // tmp1 = v + x_(j,i)*Q in P1xP1 coords
 | 
			
		||||
			v.fromP1xP1(tmp1)     // update v
 | 
			
		||||
		}
 | 
			
		||||
		tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration
 | 
			
		||||
	}
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VarTimeMultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v.
 | 
			
		||||
//
 | 
			
		||||
// Execution time depends on the inputs.
 | 
			
		||||
func (v *Point) VarTimeMultiScalarMult(scalars []*Scalar, points []*Point) *Point {
 | 
			
		||||
	if len(scalars) != len(points) {
 | 
			
		||||
		panic("edwards25519: called VarTimeMultiScalarMult with different size inputs")
 | 
			
		||||
	}
 | 
			
		||||
	checkInitialized(points...)
 | 
			
		||||
 | 
			
		||||
	// Generalize double-base NAF computation to arbitrary sizes.
 | 
			
		||||
	// Here all the points are dynamic, so we only use the smaller
 | 
			
		||||
	// tables.
 | 
			
		||||
 | 
			
		||||
	// Build lookup tables for each point
 | 
			
		||||
	tables := make([]nafLookupTable5, len(points))
 | 
			
		||||
	for i := range tables {
 | 
			
		||||
		tables[i].FromP3(points[i])
 | 
			
		||||
	}
 | 
			
		||||
	// Compute a NAF for each scalar
 | 
			
		||||
	nafs := make([][256]int8, len(scalars))
 | 
			
		||||
	for i := range nafs {
 | 
			
		||||
		nafs[i] = scalars[i].nonAdjacentForm(5)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	multiple := &projCached{}
 | 
			
		||||
	tmp1 := &projP1xP1{}
 | 
			
		||||
	tmp2 := &projP2{}
 | 
			
		||||
	tmp2.Zero()
 | 
			
		||||
 | 
			
		||||
	// Move from high to low bits, doubling the accumulator
 | 
			
		||||
	// at each iteration and checking whether there is a nonzero
 | 
			
		||||
	// coefficient to look up a multiple of.
 | 
			
		||||
	//
 | 
			
		||||
	// Skip trying to find the first nonzero coefficent, because
 | 
			
		||||
	// searching might be more work than a few extra doublings.
 | 
			
		||||
	for i := 255; i >= 0; i-- {
 | 
			
		||||
		tmp1.Double(tmp2)
 | 
			
		||||
 | 
			
		||||
		for j := range nafs {
 | 
			
		||||
			if nafs[j][i] > 0 {
 | 
			
		||||
				v.fromP1xP1(tmp1)
 | 
			
		||||
				tables[j].SelectInto(multiple, nafs[j][i])
 | 
			
		||||
				tmp1.Add(v, multiple)
 | 
			
		||||
			} else if nafs[j][i] < 0 {
 | 
			
		||||
				v.fromP1xP1(tmp1)
 | 
			
		||||
				tables[j].SelectInto(multiple, -nafs[j][i])
 | 
			
		||||
				tmp1.Sub(v, multiple)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmp2.FromP1xP1(tmp1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	v.fromP2(tmp2)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
@@ -8,6 +8,7 @@ package field
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/subtle"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"math/bits"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -92,7 +93,7 @@ func (v *Element) Add(a, b *Element) *Element {
 | 
			
		||||
	// Using the generic implementation here is actually faster than the
 | 
			
		||||
	// assembly. Probably because the body of this function is so simple that
 | 
			
		||||
	// the compiler can figure out better optimizations by inlining the carry
 | 
			
		||||
	// propagation. TODO
 | 
			
		||||
	// propagation.
 | 
			
		||||
	return v.carryPropagateGeneric()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -186,14 +187,17 @@ func (v *Element) Set(a *Element) *Element {
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetBytes sets v to x, which must be a 32-byte little-endian encoding.
 | 
			
		||||
// SetBytes sets v to x, where x is a 32-byte little-endian encoding. If x is
 | 
			
		||||
// not of the right length, SetBytes returns nil and an error, and the
 | 
			
		||||
// receiver is unchanged.
 | 
			
		||||
//
 | 
			
		||||
// Consistent with RFC 7748, the most significant bit (the high bit of the
 | 
			
		||||
// last byte) is ignored, and non-canonical values (2^255-19 through 2^255-1)
 | 
			
		||||
// are accepted. Note that this is laxer than specified by RFC 8032.
 | 
			
		||||
func (v *Element) SetBytes(x []byte) *Element {
 | 
			
		||||
// are accepted. Note that this is laxer than specified by RFC 8032, but
 | 
			
		||||
// consistent with most Ed25519 implementations.
 | 
			
		||||
func (v *Element) SetBytes(x []byte) (*Element, error) {
 | 
			
		||||
	if len(x) != 32 {
 | 
			
		||||
		panic("edwards25519: invalid field element input size")
 | 
			
		||||
		return nil, errors.New("edwards25519: invalid field element input size")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Bits 0:51 (bytes 0:8, bits 0:64, shift 0, mask 51).
 | 
			
		||||
@@ -208,12 +212,12 @@ func (v *Element) SetBytes(x []byte) *Element {
 | 
			
		||||
	// Bits 153:204 (bytes 19:27, bits 152:216, shift 1, mask 51).
 | 
			
		||||
	v.l3 = binary.LittleEndian.Uint64(x[19:27]) >> 1
 | 
			
		||||
	v.l3 &= maskLow51Bits
 | 
			
		||||
	// Bits 204:251 (bytes 24:32, bits 192:256, shift 12, mask 51).
 | 
			
		||||
	// Bits 204:255 (bytes 24:32, bits 192:256, shift 12, mask 51).
 | 
			
		||||
	// Note: not bytes 25:33, shift 4, to avoid overread.
 | 
			
		||||
	v.l4 = binary.LittleEndian.Uint64(x[24:32]) >> 12
 | 
			
		||||
	v.l4 &= maskLow51Bits
 | 
			
		||||
 | 
			
		||||
	return v
 | 
			
		||||
	return v, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Bytes returns the canonical 32-byte little-endian encoding of v.
 | 
			
		||||
@@ -391,26 +395,26 @@ var sqrtM1 = &Element{1718705420411056, 234908883556509,
 | 
			
		||||
// If u/v is square, SqrtRatio returns r and 1. If u/v is not square, SqrtRatio
 | 
			
		||||
// sets r according to Section 4.3 of draft-irtf-cfrg-ristretto255-decaf448-00,
 | 
			
		||||
// and returns r and 0.
 | 
			
		||||
func (r *Element) SqrtRatio(u, v *Element) (rr *Element, wasSquare int) {
 | 
			
		||||
	var a, b Element
 | 
			
		||||
func (r *Element) SqrtRatio(u, v *Element) (R *Element, wasSquare int) {
 | 
			
		||||
	t0 := new(Element)
 | 
			
		||||
 | 
			
		||||
	// r = (u * v3) * (u * v7)^((p-5)/8)
 | 
			
		||||
	v2 := a.Square(v)
 | 
			
		||||
	uv3 := b.Multiply(u, b.Multiply(v2, v))
 | 
			
		||||
	uv7 := a.Multiply(uv3, a.Square(v2))
 | 
			
		||||
	r.Multiply(uv3, r.Pow22523(uv7))
 | 
			
		||||
	v2 := new(Element).Square(v)
 | 
			
		||||
	uv3 := new(Element).Multiply(u, t0.Multiply(v2, v))
 | 
			
		||||
	uv7 := new(Element).Multiply(uv3, t0.Square(v2))
 | 
			
		||||
	rr := new(Element).Multiply(uv3, t0.Pow22523(uv7))
 | 
			
		||||
 | 
			
		||||
	check := a.Multiply(v, a.Square(r)) // check = v * r^2
 | 
			
		||||
	check := new(Element).Multiply(v, t0.Square(rr)) // check = v * r^2
 | 
			
		||||
 | 
			
		||||
	uNeg := b.Negate(u)
 | 
			
		||||
	uNeg := new(Element).Negate(u)
 | 
			
		||||
	correctSignSqrt := check.Equal(u)
 | 
			
		||||
	flippedSignSqrt := check.Equal(uNeg)
 | 
			
		||||
	flippedSignSqrtI := check.Equal(uNeg.Multiply(uNeg, sqrtM1))
 | 
			
		||||
	flippedSignSqrtI := check.Equal(t0.Multiply(uNeg, sqrtM1))
 | 
			
		||||
 | 
			
		||||
	rPrime := b.Multiply(r, sqrtM1) // r_prime = SQRT_M1 * r
 | 
			
		||||
	rPrime := new(Element).Multiply(rr, sqrtM1) // r_prime = SQRT_M1 * r
 | 
			
		||||
	// r = CT_SELECT(r_prime IF flipped_sign_sqrt | flipped_sign_sqrt_i ELSE r)
 | 
			
		||||
	r.Select(rPrime, r, flippedSignSqrt|flippedSignSqrtI)
 | 
			
		||||
	rr.Select(rPrime, rr, flippedSignSqrt|flippedSignSqrtI)
 | 
			
		||||
 | 
			
		||||
	r.Absolute(r) // Choose the nonnegative square root.
 | 
			
		||||
	r.Absolute(rr) // Choose the nonnegative square root.
 | 
			
		||||
	return r, correctSignSqrt | flippedSignSqrt
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +1,16 @@
 | 
			
		||||
// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT.
 | 
			
		||||
 | 
			
		||||
//go:build amd64 && gc && !purego
 | 
			
		||||
// +build amd64,gc,!purego
 | 
			
		||||
 | 
			
		||||
package field
 | 
			
		||||
 | 
			
		||||
// feMul sets out = a * b. It works like feMulGeneric.
 | 
			
		||||
//
 | 
			
		||||
//go:noescape
 | 
			
		||||
func feMul(out *Element, a *Element, b *Element)
 | 
			
		||||
 | 
			
		||||
// feSquare sets out = a * a. It works like feSquareGeneric.
 | 
			
		||||
//
 | 
			
		||||
//go:noescape
 | 
			
		||||
func feSquare(out *Element, a *Element)
 | 
			
		||||
@@ -3,7 +3,6 @@
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
//go:build arm64 && gc && !purego
 | 
			
		||||
// +build arm64,gc,!purego
 | 
			
		||||
 | 
			
		||||
#include "textflag.h"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										50
									
								
								vendor/filippo.io/edwards25519/field/fe_extra.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								vendor/filippo.io/edwards25519/field/fe_extra.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
// Copyright (c) 2021 The Go Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package field
 | 
			
		||||
 | 
			
		||||
import "errors"
 | 
			
		||||
 | 
			
		||||
// This file contains additional functionality that is not included in the
 | 
			
		||||
// upstream crypto/ed25519/edwards25519/field package.
 | 
			
		||||
 | 
			
		||||
// SetWideBytes sets v to x, where x is a 64-byte little-endian encoding, which
 | 
			
		||||
// is reduced modulo the field order. If x is not of the right length,
 | 
			
		||||
// SetWideBytes returns nil and an error, and the receiver is unchanged.
 | 
			
		||||
//
 | 
			
		||||
// SetWideBytes is not necessary to select a uniformly distributed value, and is
 | 
			
		||||
// only provided for compatibility: SetBytes can be used instead as the chance
 | 
			
		||||
// of bias is less than 2⁻²⁵⁰.
 | 
			
		||||
func (v *Element) SetWideBytes(x []byte) (*Element, error) {
 | 
			
		||||
	if len(x) != 64 {
 | 
			
		||||
		return nil, errors.New("edwards25519: invalid SetWideBytes input size")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Split the 64 bytes into two elements, and extract the most significant
 | 
			
		||||
	// bit of each, which is ignored by SetBytes.
 | 
			
		||||
	lo, _ := new(Element).SetBytes(x[:32])
 | 
			
		||||
	loMSB := uint64(x[31] >> 7)
 | 
			
		||||
	hi, _ := new(Element).SetBytes(x[32:])
 | 
			
		||||
	hiMSB := uint64(x[63] >> 7)
 | 
			
		||||
 | 
			
		||||
	// The output we want is
 | 
			
		||||
	//
 | 
			
		||||
	//   v = lo + loMSB * 2²⁵⁵ + hi * 2²⁵⁶ + hiMSB * 2⁵¹¹
 | 
			
		||||
	//
 | 
			
		||||
	// which applying the reduction identity comes out to
 | 
			
		||||
	//
 | 
			
		||||
	//   v = lo + loMSB * 19 + hi * 2 * 19 + hiMSB * 2 * 19²
 | 
			
		||||
	//
 | 
			
		||||
	// l0 will be the sum of a 52 bits value (lo.l0), plus a 5 bits value
 | 
			
		||||
	// (loMSB * 19), a 6 bits value (hi.l0 * 2 * 19), and a 10 bits value
 | 
			
		||||
	// (hiMSB * 2 * 19²), so it fits in a uint64.
 | 
			
		||||
 | 
			
		||||
	v.l0 = lo.l0 + loMSB*19 + hi.l0*2*19 + hiMSB*2*19*19
 | 
			
		||||
	v.l1 = lo.l1 + hi.l1*2*19
 | 
			
		||||
	v.l2 = lo.l2 + hi.l2*2*19
 | 
			
		||||
	v.l3 = lo.l3 + hi.l3*2*19
 | 
			
		||||
	v.l4 = lo.l4 + hi.l4*2*19
 | 
			
		||||
 | 
			
		||||
	return v.carryPropagate(), nil
 | 
			
		||||
}
 | 
			
		||||
@@ -156,7 +156,7 @@ func feMulGeneric(v, a, b *Element) {
 | 
			
		||||
	rr4 := r4.lo&maskLow51Bits + c3
 | 
			
		||||
 | 
			
		||||
	// Now all coefficients fit into 64-bit registers but are still too large to
 | 
			
		||||
	// be passed around as a Element. We therefore do one last carry chain,
 | 
			
		||||
	// be passed around as an Element. We therefore do one last carry chain,
 | 
			
		||||
	// where the carries will be small enough to fit in the wiggle room above 2⁵¹.
 | 
			
		||||
	*v = Element{rr0, rr1, rr2, rr3, rr4}
 | 
			
		||||
	v.carryPropagate()
 | 
			
		||||
@@ -245,8 +245,8 @@ func feSquareGeneric(v, a *Element) {
 | 
			
		||||
	v.carryPropagate()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// carryPropagate brings the limbs below 52 bits by applying the reduction
 | 
			
		||||
// identity (a * 2²⁵⁵ + b = a * 19 + b) to the l4 carry. TODO inline
 | 
			
		||||
// carryPropagateGeneric brings the limbs below 52 bits by applying the reduction
 | 
			
		||||
// identity (a * 2²⁵⁵ + b = a * 19 + b) to the l4 carry.
 | 
			
		||||
func (v *Element) carryPropagateGeneric() *Element {
 | 
			
		||||
	c0 := v.l0 >> 51
 | 
			
		||||
	c1 := v.l1 >> 51
 | 
			
		||||
@@ -254,6 +254,8 @@ func (v *Element) carryPropagateGeneric() *Element {
 | 
			
		||||
	c3 := v.l3 >> 51
 | 
			
		||||
	c4 := v.l4 >> 51
 | 
			
		||||
 | 
			
		||||
	// c4 is at most 64 - 51 = 13 bits, so c4*19 is at most 18 bits, and
 | 
			
		||||
	// the final l0 will be at most 52 bits. Similarly for the rest.
 | 
			
		||||
	v.l0 = v.l0&maskLow51Bits + c4*19
 | 
			
		||||
	v.l1 = v.l1&maskLow51Bits + c0
 | 
			
		||||
	v.l2 = v.l2&maskLow51Bits + c1
 | 
			
		||||
							
								
								
									
										343
									
								
								vendor/filippo.io/edwards25519/scalar.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								vendor/filippo.io/edwards25519/scalar.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,343 @@
 | 
			
		||||
// Copyright (c) 2016 The Go Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package edwards25519
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// A Scalar is an integer modulo
 | 
			
		||||
//
 | 
			
		||||
//	l = 2^252 + 27742317777372353535851937790883648493
 | 
			
		||||
//
 | 
			
		||||
// which is the prime order of the edwards25519 group.
 | 
			
		||||
//
 | 
			
		||||
// This type works similarly to math/big.Int, and all arguments and
 | 
			
		||||
// receivers are allowed to alias.
 | 
			
		||||
//
 | 
			
		||||
// The zero value is a valid zero element.
 | 
			
		||||
type Scalar struct {
 | 
			
		||||
	// s is the scalar in the Montgomery domain, in the format of the
 | 
			
		||||
	// fiat-crypto implementation.
 | 
			
		||||
	s fiatScalarMontgomeryDomainFieldElement
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// The field implementation in scalar_fiat.go is generated by the fiat-crypto
 | 
			
		||||
// project (https://github.com/mit-plv/fiat-crypto) at version v0.0.9 (23d2dbc)
 | 
			
		||||
// from a formally verified model.
 | 
			
		||||
//
 | 
			
		||||
// fiat-crypto code comes under the following license.
 | 
			
		||||
//
 | 
			
		||||
//     Copyright (c) 2015-2020 The fiat-crypto Authors. All rights reserved.
 | 
			
		||||
//
 | 
			
		||||
//     Redistribution and use in source and binary forms, with or without
 | 
			
		||||
//     modification, are permitted provided that the following conditions are
 | 
			
		||||
//     met:
 | 
			
		||||
//
 | 
			
		||||
//         1. Redistributions of source code must retain the above copyright
 | 
			
		||||
//         notice, this list of conditions and the following disclaimer.
 | 
			
		||||
//
 | 
			
		||||
//     THIS SOFTWARE IS PROVIDED BY the fiat-crypto authors "AS IS"
 | 
			
		||||
//     AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 | 
			
		||||
//     THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 | 
			
		||||
//     PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Berkeley Software Design,
 | 
			
		||||
//     Inc. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 | 
			
		||||
//     EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 | 
			
		||||
//     PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 | 
			
		||||
//     PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 | 
			
		||||
//     LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 | 
			
		||||
//     NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 | 
			
		||||
//     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
// NewScalar returns a new zero Scalar.
 | 
			
		||||
func NewScalar() *Scalar {
 | 
			
		||||
	return &Scalar{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MultiplyAdd sets s = x * y + z mod l, and returns s. It is equivalent to
 | 
			
		||||
// using Multiply and then Add.
 | 
			
		||||
func (s *Scalar) MultiplyAdd(x, y, z *Scalar) *Scalar {
 | 
			
		||||
	// Make a copy of z in case it aliases s.
 | 
			
		||||
	zCopy := new(Scalar).Set(z)
 | 
			
		||||
	return s.Multiply(x, y).Add(s, zCopy)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Add sets s = x + y mod l, and returns s.
 | 
			
		||||
func (s *Scalar) Add(x, y *Scalar) *Scalar {
 | 
			
		||||
	// s = 1 * x + y mod l
 | 
			
		||||
	fiatScalarAdd(&s.s, &x.s, &y.s)
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Subtract sets s = x - y mod l, and returns s.
 | 
			
		||||
func (s *Scalar) Subtract(x, y *Scalar) *Scalar {
 | 
			
		||||
	// s = -1 * y + x mod l
 | 
			
		||||
	fiatScalarSub(&s.s, &x.s, &y.s)
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Negate sets s = -x mod l, and returns s.
 | 
			
		||||
func (s *Scalar) Negate(x *Scalar) *Scalar {
 | 
			
		||||
	// s = -1 * x + 0 mod l
 | 
			
		||||
	fiatScalarOpp(&s.s, &x.s)
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Multiply sets s = x * y mod l, and returns s.
 | 
			
		||||
func (s *Scalar) Multiply(x, y *Scalar) *Scalar {
 | 
			
		||||
	// s = x * y + 0 mod l
 | 
			
		||||
	fiatScalarMul(&s.s, &x.s, &y.s)
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set sets s = x, and returns s.
 | 
			
		||||
func (s *Scalar) Set(x *Scalar) *Scalar {
 | 
			
		||||
	*s = *x
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetUniformBytes sets s = x mod l, where x is a 64-byte little-endian integer.
 | 
			
		||||
// If x is not of the right length, SetUniformBytes returns nil and an error,
 | 
			
		||||
// and the receiver is unchanged.
 | 
			
		||||
//
 | 
			
		||||
// SetUniformBytes can be used to set s to a uniformly distributed value given
 | 
			
		||||
// 64 uniformly distributed random bytes.
 | 
			
		||||
func (s *Scalar) SetUniformBytes(x []byte) (*Scalar, error) {
 | 
			
		||||
	if len(x) != 64 {
 | 
			
		||||
		return nil, errors.New("edwards25519: invalid SetUniformBytes input length")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// We have a value x of 512 bits, but our fiatScalarFromBytes function
 | 
			
		||||
	// expects an input lower than l, which is a little over 252 bits.
 | 
			
		||||
	//
 | 
			
		||||
	// Instead of writing a reduction function that operates on wider inputs, we
 | 
			
		||||
	// can interpret x as the sum of three shorter values a, b, and c.
 | 
			
		||||
	//
 | 
			
		||||
	//    x = a + b * 2^168 + c * 2^336  mod l
 | 
			
		||||
	//
 | 
			
		||||
	// We then precompute 2^168 and 2^336 modulo l, and perform the reduction
 | 
			
		||||
	// with two multiplications and two additions.
 | 
			
		||||
 | 
			
		||||
	s.setShortBytes(x[:21])
 | 
			
		||||
	t := new(Scalar).setShortBytes(x[21:42])
 | 
			
		||||
	s.Add(s, t.Multiply(t, scalarTwo168))
 | 
			
		||||
	t.setShortBytes(x[42:])
 | 
			
		||||
	s.Add(s, t.Multiply(t, scalarTwo336))
 | 
			
		||||
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// scalarTwo168 and scalarTwo336 are 2^168 and 2^336 modulo l, encoded as a
 | 
			
		||||
// fiatScalarMontgomeryDomainFieldElement, which is a little-endian 4-limb value
 | 
			
		||||
// in the 2^256 Montgomery domain.
 | 
			
		||||
var scalarTwo168 = &Scalar{s: [4]uint64{0x5b8ab432eac74798, 0x38afddd6de59d5d7,
 | 
			
		||||
	0xa2c131b399411b7c, 0x6329a7ed9ce5a30}}
 | 
			
		||||
var scalarTwo336 = &Scalar{s: [4]uint64{0xbd3d108e2b35ecc5, 0x5c3a3718bdf9c90b,
 | 
			
		||||
	0x63aa97a331b4f2ee, 0x3d217f5be65cb5c}}
 | 
			
		||||
 | 
			
		||||
// setShortBytes sets s = x mod l, where x is a little-endian integer shorter
 | 
			
		||||
// than 32 bytes.
 | 
			
		||||
func (s *Scalar) setShortBytes(x []byte) *Scalar {
 | 
			
		||||
	if len(x) >= 32 {
 | 
			
		||||
		panic("edwards25519: internal error: setShortBytes called with a long string")
 | 
			
		||||
	}
 | 
			
		||||
	var buf [32]byte
 | 
			
		||||
	copy(buf[:], x)
 | 
			
		||||
	fiatScalarFromBytes((*[4]uint64)(&s.s), &buf)
 | 
			
		||||
	fiatScalarToMontgomery(&s.s, (*fiatScalarNonMontgomeryDomainFieldElement)(&s.s))
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetCanonicalBytes sets s = x, where x is a 32-byte little-endian encoding of
 | 
			
		||||
// s, and returns s. If x is not a canonical encoding of s, SetCanonicalBytes
 | 
			
		||||
// returns nil and an error, and the receiver is unchanged.
 | 
			
		||||
func (s *Scalar) SetCanonicalBytes(x []byte) (*Scalar, error) {
 | 
			
		||||
	if len(x) != 32 {
 | 
			
		||||
		return nil, errors.New("invalid scalar length")
 | 
			
		||||
	}
 | 
			
		||||
	if !isReduced(x) {
 | 
			
		||||
		return nil, errors.New("invalid scalar encoding")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fiatScalarFromBytes((*[4]uint64)(&s.s), (*[32]byte)(x))
 | 
			
		||||
	fiatScalarToMontgomery(&s.s, (*fiatScalarNonMontgomeryDomainFieldElement)(&s.s))
 | 
			
		||||
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// scalarMinusOneBytes is l - 1 in little endian.
 | 
			
		||||
var scalarMinusOneBytes = [32]byte{236, 211, 245, 92, 26, 99, 18, 88, 214, 156, 247, 162, 222, 249, 222, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16}
 | 
			
		||||
 | 
			
		||||
// isReduced returns whether the given scalar in 32-byte little endian encoded
 | 
			
		||||
// form is reduced modulo l.
 | 
			
		||||
func isReduced(s []byte) bool {
 | 
			
		||||
	if len(s) != 32 {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := len(s) - 1; i >= 0; i-- {
 | 
			
		||||
		switch {
 | 
			
		||||
		case s[i] > scalarMinusOneBytes[i]:
 | 
			
		||||
			return false
 | 
			
		||||
		case s[i] < scalarMinusOneBytes[i]:
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetBytesWithClamping applies the buffer pruning described in RFC 8032,
 | 
			
		||||
// Section 5.1.5 (also known as clamping) and sets s to the result. The input
 | 
			
		||||
// must be 32 bytes, and it is not modified. If x is not of the right length,
 | 
			
		||||
// SetBytesWithClamping returns nil and an error, and the receiver is unchanged.
 | 
			
		||||
//
 | 
			
		||||
// Note that since Scalar values are always reduced modulo the prime order of
 | 
			
		||||
// the curve, the resulting value will not preserve any of the cofactor-clearing
 | 
			
		||||
// properties that clamping is meant to provide. It will however work as
 | 
			
		||||
// expected as long as it is applied to points on the prime order subgroup, like
 | 
			
		||||
// in Ed25519. In fact, it is lost to history why RFC 8032 adopted the
 | 
			
		||||
// irrelevant RFC 7748 clamping, but it is now required for compatibility.
 | 
			
		||||
func (s *Scalar) SetBytesWithClamping(x []byte) (*Scalar, error) {
 | 
			
		||||
	// The description above omits the purpose of the high bits of the clamping
 | 
			
		||||
	// for brevity, but those are also lost to reductions, and are also
 | 
			
		||||
	// irrelevant to edwards25519 as they protect against a specific
 | 
			
		||||
	// implementation bug that was once observed in a generic Montgomery ladder.
 | 
			
		||||
	if len(x) != 32 {
 | 
			
		||||
		return nil, errors.New("edwards25519: invalid SetBytesWithClamping input length")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// We need to use the wide reduction from SetUniformBytes, since clamping
 | 
			
		||||
	// sets the 2^254 bit, making the value higher than the order.
 | 
			
		||||
	var wideBytes [64]byte
 | 
			
		||||
	copy(wideBytes[:], x[:])
 | 
			
		||||
	wideBytes[0] &= 248
 | 
			
		||||
	wideBytes[31] &= 63
 | 
			
		||||
	wideBytes[31] |= 64
 | 
			
		||||
	return s.SetUniformBytes(wideBytes[:])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Bytes returns the canonical 32-byte little-endian encoding of s.
 | 
			
		||||
func (s *Scalar) Bytes() []byte {
 | 
			
		||||
	// This function is outlined to make the allocations inline in the caller
 | 
			
		||||
	// rather than happen on the heap.
 | 
			
		||||
	var encoded [32]byte
 | 
			
		||||
	return s.bytes(&encoded)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Scalar) bytes(out *[32]byte) []byte {
 | 
			
		||||
	var ss fiatScalarNonMontgomeryDomainFieldElement
 | 
			
		||||
	fiatScalarFromMontgomery(&ss, &s.s)
 | 
			
		||||
	fiatScalarToBytes(out, (*[4]uint64)(&ss))
 | 
			
		||||
	return out[:]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Equal returns 1 if s and t are equal, and 0 otherwise.
 | 
			
		||||
func (s *Scalar) Equal(t *Scalar) int {
 | 
			
		||||
	var diff fiatScalarMontgomeryDomainFieldElement
 | 
			
		||||
	fiatScalarSub(&diff, &s.s, &t.s)
 | 
			
		||||
	var nonzero uint64
 | 
			
		||||
	fiatScalarNonzero(&nonzero, (*[4]uint64)(&diff))
 | 
			
		||||
	nonzero |= nonzero >> 32
 | 
			
		||||
	nonzero |= nonzero >> 16
 | 
			
		||||
	nonzero |= nonzero >> 8
 | 
			
		||||
	nonzero |= nonzero >> 4
 | 
			
		||||
	nonzero |= nonzero >> 2
 | 
			
		||||
	nonzero |= nonzero >> 1
 | 
			
		||||
	return int(^nonzero) & 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// nonAdjacentForm computes a width-w non-adjacent form for this scalar.
 | 
			
		||||
//
 | 
			
		||||
// w must be between 2 and 8, or nonAdjacentForm will panic.
 | 
			
		||||
func (s *Scalar) nonAdjacentForm(w uint) [256]int8 {
 | 
			
		||||
	// This implementation is adapted from the one
 | 
			
		||||
	// in curve25519-dalek and is documented there:
 | 
			
		||||
	// https://github.com/dalek-cryptography/curve25519-dalek/blob/f630041af28e9a405255f98a8a93adca18e4315b/src/scalar.rs#L800-L871
 | 
			
		||||
	b := s.Bytes()
 | 
			
		||||
	if b[31] > 127 {
 | 
			
		||||
		panic("scalar has high bit set illegally")
 | 
			
		||||
	}
 | 
			
		||||
	if w < 2 {
 | 
			
		||||
		panic("w must be at least 2 by the definition of NAF")
 | 
			
		||||
	} else if w > 8 {
 | 
			
		||||
		panic("NAF digits must fit in int8")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var naf [256]int8
 | 
			
		||||
	var digits [5]uint64
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < 4; i++ {
 | 
			
		||||
		digits[i] = binary.LittleEndian.Uint64(b[i*8:])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	width := uint64(1 << w)
 | 
			
		||||
	windowMask := uint64(width - 1)
 | 
			
		||||
 | 
			
		||||
	pos := uint(0)
 | 
			
		||||
	carry := uint64(0)
 | 
			
		||||
	for pos < 256 {
 | 
			
		||||
		indexU64 := pos / 64
 | 
			
		||||
		indexBit := pos % 64
 | 
			
		||||
		var bitBuf uint64
 | 
			
		||||
		if indexBit < 64-w {
 | 
			
		||||
			// This window's bits are contained in a single u64
 | 
			
		||||
			bitBuf = digits[indexU64] >> indexBit
 | 
			
		||||
		} else {
 | 
			
		||||
			// Combine the current 64 bits with bits from the next 64
 | 
			
		||||
			bitBuf = (digits[indexU64] >> indexBit) | (digits[1+indexU64] << (64 - indexBit))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Add carry into the current window
 | 
			
		||||
		window := carry + (bitBuf & windowMask)
 | 
			
		||||
 | 
			
		||||
		if window&1 == 0 {
 | 
			
		||||
			// If the window value is even, preserve the carry and continue.
 | 
			
		||||
			// Why is the carry preserved?
 | 
			
		||||
			// If carry == 0 and window & 1 == 0,
 | 
			
		||||
			//    then the next carry should be 0
 | 
			
		||||
			// If carry == 1 and window & 1 == 0,
 | 
			
		||||
			//    then bit_buf & 1 == 1 so the next carry should be 1
 | 
			
		||||
			pos += 1
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if window < width/2 {
 | 
			
		||||
			carry = 0
 | 
			
		||||
			naf[pos] = int8(window)
 | 
			
		||||
		} else {
 | 
			
		||||
			carry = 1
 | 
			
		||||
			naf[pos] = int8(window) - int8(width)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		pos += w
 | 
			
		||||
	}
 | 
			
		||||
	return naf
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Scalar) signedRadix16() [64]int8 {
 | 
			
		||||
	b := s.Bytes()
 | 
			
		||||
	if b[31] > 127 {
 | 
			
		||||
		panic("scalar has high bit set illegally")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var digits [64]int8
 | 
			
		||||
 | 
			
		||||
	// Compute unsigned radix-16 digits:
 | 
			
		||||
	for i := 0; i < 32; i++ {
 | 
			
		||||
		digits[2*i] = int8(b[i] & 15)
 | 
			
		||||
		digits[2*i+1] = int8((b[i] >> 4) & 15)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Recenter coefficients:
 | 
			
		||||
	for i := 0; i < 63; i++ {
 | 
			
		||||
		carry := (digits[i] + 8) >> 4
 | 
			
		||||
		digits[i] -= carry << 4
 | 
			
		||||
		digits[i+1] += carry
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return digits
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1147
									
								
								vendor/filippo.io/edwards25519/scalar_fiat.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1147
									
								
								vendor/filippo.io/edwards25519/scalar_fiat.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										214
									
								
								vendor/filippo.io/edwards25519/scalarmult.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								vendor/filippo.io/edwards25519/scalarmult.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,214 @@
 | 
			
		||||
// Copyright (c) 2019 The Go Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package edwards25519
 | 
			
		||||
 | 
			
		||||
import "sync"
 | 
			
		||||
 | 
			
		||||
// basepointTable is a set of 32 affineLookupTables, where table i is generated
 | 
			
		||||
// from 256i * basepoint. It is precomputed the first time it's used.
 | 
			
		||||
func basepointTable() *[32]affineLookupTable {
 | 
			
		||||
	basepointTablePrecomp.initOnce.Do(func() {
 | 
			
		||||
		p := NewGeneratorPoint()
 | 
			
		||||
		for i := 0; i < 32; i++ {
 | 
			
		||||
			basepointTablePrecomp.table[i].FromP3(p)
 | 
			
		||||
			for j := 0; j < 8; j++ {
 | 
			
		||||
				p.Add(p, p)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	return &basepointTablePrecomp.table
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var basepointTablePrecomp struct {
 | 
			
		||||
	table    [32]affineLookupTable
 | 
			
		||||
	initOnce sync.Once
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ScalarBaseMult sets v = x * B, where B is the canonical generator, and
 | 
			
		||||
// returns v.
 | 
			
		||||
//
 | 
			
		||||
// The scalar multiplication is done in constant time.
 | 
			
		||||
func (v *Point) ScalarBaseMult(x *Scalar) *Point {
 | 
			
		||||
	basepointTable := basepointTable()
 | 
			
		||||
 | 
			
		||||
	// Write x = sum(x_i * 16^i) so  x*B = sum( B*x_i*16^i )
 | 
			
		||||
	// as described in the Ed25519 paper
 | 
			
		||||
	//
 | 
			
		||||
	// Group even and odd coefficients
 | 
			
		||||
	// x*B     = x_0*16^0*B + x_2*16^2*B + ... + x_62*16^62*B
 | 
			
		||||
	//         + x_1*16^1*B + x_3*16^3*B + ... + x_63*16^63*B
 | 
			
		||||
	// x*B     = x_0*16^0*B + x_2*16^2*B + ... + x_62*16^62*B
 | 
			
		||||
	//    + 16*( x_1*16^0*B + x_3*16^2*B + ... + x_63*16^62*B)
 | 
			
		||||
	//
 | 
			
		||||
	// We use a lookup table for each i to get x_i*16^(2*i)*B
 | 
			
		||||
	// and do four doublings to multiply by 16.
 | 
			
		||||
	digits := x.signedRadix16()
 | 
			
		||||
 | 
			
		||||
	multiple := &affineCached{}
 | 
			
		||||
	tmp1 := &projP1xP1{}
 | 
			
		||||
	tmp2 := &projP2{}
 | 
			
		||||
 | 
			
		||||
	// Accumulate the odd components first
 | 
			
		||||
	v.Set(NewIdentityPoint())
 | 
			
		||||
	for i := 1; i < 64; i += 2 {
 | 
			
		||||
		basepointTable[i/2].SelectInto(multiple, digits[i])
 | 
			
		||||
		tmp1.AddAffine(v, multiple)
 | 
			
		||||
		v.fromP1xP1(tmp1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Multiply by 16
 | 
			
		||||
	tmp2.FromP3(v)       // tmp2 =    v in P2 coords
 | 
			
		||||
	tmp1.Double(tmp2)    // tmp1 =  2*v in P1xP1 coords
 | 
			
		||||
	tmp2.FromP1xP1(tmp1) // tmp2 =  2*v in P2 coords
 | 
			
		||||
	tmp1.Double(tmp2)    // tmp1 =  4*v in P1xP1 coords
 | 
			
		||||
	tmp2.FromP1xP1(tmp1) // tmp2 =  4*v in P2 coords
 | 
			
		||||
	tmp1.Double(tmp2)    // tmp1 =  8*v in P1xP1 coords
 | 
			
		||||
	tmp2.FromP1xP1(tmp1) // tmp2 =  8*v in P2 coords
 | 
			
		||||
	tmp1.Double(tmp2)    // tmp1 = 16*v in P1xP1 coords
 | 
			
		||||
	v.fromP1xP1(tmp1)    // now v = 16*(odd components)
 | 
			
		||||
 | 
			
		||||
	// Accumulate the even components
 | 
			
		||||
	for i := 0; i < 64; i += 2 {
 | 
			
		||||
		basepointTable[i/2].SelectInto(multiple, digits[i])
 | 
			
		||||
		tmp1.AddAffine(v, multiple)
 | 
			
		||||
		v.fromP1xP1(tmp1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ScalarMult sets v = x * q, and returns v.
 | 
			
		||||
//
 | 
			
		||||
// The scalar multiplication is done in constant time.
 | 
			
		||||
func (v *Point) ScalarMult(x *Scalar, q *Point) *Point {
 | 
			
		||||
	checkInitialized(q)
 | 
			
		||||
 | 
			
		||||
	var table projLookupTable
 | 
			
		||||
	table.FromP3(q)
 | 
			
		||||
 | 
			
		||||
	// Write x = sum(x_i * 16^i)
 | 
			
		||||
	// so  x*Q = sum( Q*x_i*16^i )
 | 
			
		||||
	//         = Q*x_0 + 16*(Q*x_1 + 16*( ... + Q*x_63) ... )
 | 
			
		||||
	//           <------compute inside out---------
 | 
			
		||||
	//
 | 
			
		||||
	// We use the lookup table to get the x_i*Q values
 | 
			
		||||
	// and do four doublings to compute 16*Q
 | 
			
		||||
	digits := x.signedRadix16()
 | 
			
		||||
 | 
			
		||||
	// Unwrap first loop iteration to save computing 16*identity
 | 
			
		||||
	multiple := &projCached{}
 | 
			
		||||
	tmp1 := &projP1xP1{}
 | 
			
		||||
	tmp2 := &projP2{}
 | 
			
		||||
	table.SelectInto(multiple, digits[63])
 | 
			
		||||
 | 
			
		||||
	v.Set(NewIdentityPoint())
 | 
			
		||||
	tmp1.Add(v, multiple) // tmp1 = x_63*Q in P1xP1 coords
 | 
			
		||||
	for i := 62; i >= 0; i-- {
 | 
			
		||||
		tmp2.FromP1xP1(tmp1) // tmp2 =    (prev) in P2 coords
 | 
			
		||||
		tmp1.Double(tmp2)    // tmp1 =  2*(prev) in P1xP1 coords
 | 
			
		||||
		tmp2.FromP1xP1(tmp1) // tmp2 =  2*(prev) in P2 coords
 | 
			
		||||
		tmp1.Double(tmp2)    // tmp1 =  4*(prev) in P1xP1 coords
 | 
			
		||||
		tmp2.FromP1xP1(tmp1) // tmp2 =  4*(prev) in P2 coords
 | 
			
		||||
		tmp1.Double(tmp2)    // tmp1 =  8*(prev) in P1xP1 coords
 | 
			
		||||
		tmp2.FromP1xP1(tmp1) // tmp2 =  8*(prev) in P2 coords
 | 
			
		||||
		tmp1.Double(tmp2)    // tmp1 = 16*(prev) in P1xP1 coords
 | 
			
		||||
		v.fromP1xP1(tmp1)    //    v = 16*(prev) in P3 coords
 | 
			
		||||
		table.SelectInto(multiple, digits[i])
 | 
			
		||||
		tmp1.Add(v, multiple) // tmp1 = x_i*Q + 16*(prev) in P1xP1 coords
 | 
			
		||||
	}
 | 
			
		||||
	v.fromP1xP1(tmp1)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// basepointNafTable is the nafLookupTable8 for the basepoint.
 | 
			
		||||
// It is precomputed the first time it's used.
 | 
			
		||||
func basepointNafTable() *nafLookupTable8 {
 | 
			
		||||
	basepointNafTablePrecomp.initOnce.Do(func() {
 | 
			
		||||
		basepointNafTablePrecomp.table.FromP3(NewGeneratorPoint())
 | 
			
		||||
	})
 | 
			
		||||
	return &basepointNafTablePrecomp.table
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var basepointNafTablePrecomp struct {
 | 
			
		||||
	table    nafLookupTable8
 | 
			
		||||
	initOnce sync.Once
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VarTimeDoubleScalarBaseMult sets v = a * A + b * B, where B is the canonical
 | 
			
		||||
// generator, and returns v.
 | 
			
		||||
//
 | 
			
		||||
// Execution time depends on the inputs.
 | 
			
		||||
func (v *Point) VarTimeDoubleScalarBaseMult(a *Scalar, A *Point, b *Scalar) *Point {
 | 
			
		||||
	checkInitialized(A)
 | 
			
		||||
 | 
			
		||||
	// Similarly to the single variable-base approach, we compute
 | 
			
		||||
	// digits and use them with a lookup table.  However, because
 | 
			
		||||
	// we are allowed to do variable-time operations, we don't
 | 
			
		||||
	// need constant-time lookups or constant-time digit
 | 
			
		||||
	// computations.
 | 
			
		||||
	//
 | 
			
		||||
	// So we use a non-adjacent form of some width w instead of
 | 
			
		||||
	// radix 16.  This is like a binary representation (one digit
 | 
			
		||||
	// for each binary place) but we allow the digits to grow in
 | 
			
		||||
	// magnitude up to 2^{w-1} so that the nonzero digits are as
 | 
			
		||||
	// sparse as possible.  Intuitively, this "condenses" the
 | 
			
		||||
	// "mass" of the scalar onto sparse coefficients (meaning
 | 
			
		||||
	// fewer additions).
 | 
			
		||||
 | 
			
		||||
	basepointNafTable := basepointNafTable()
 | 
			
		||||
	var aTable nafLookupTable5
 | 
			
		||||
	aTable.FromP3(A)
 | 
			
		||||
	// Because the basepoint is fixed, we can use a wider NAF
 | 
			
		||||
	// corresponding to a bigger table.
 | 
			
		||||
	aNaf := a.nonAdjacentForm(5)
 | 
			
		||||
	bNaf := b.nonAdjacentForm(8)
 | 
			
		||||
 | 
			
		||||
	// Find the first nonzero coefficient.
 | 
			
		||||
	i := 255
 | 
			
		||||
	for j := i; j >= 0; j-- {
 | 
			
		||||
		if aNaf[j] != 0 || bNaf[j] != 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	multA := &projCached{}
 | 
			
		||||
	multB := &affineCached{}
 | 
			
		||||
	tmp1 := &projP1xP1{}
 | 
			
		||||
	tmp2 := &projP2{}
 | 
			
		||||
	tmp2.Zero()
 | 
			
		||||
 | 
			
		||||
	// Move from high to low bits, doubling the accumulator
 | 
			
		||||
	// at each iteration and checking whether there is a nonzero
 | 
			
		||||
	// coefficient to look up a multiple of.
 | 
			
		||||
	for ; i >= 0; i-- {
 | 
			
		||||
		tmp1.Double(tmp2)
 | 
			
		||||
 | 
			
		||||
		// Only update v if we have a nonzero coeff to add in.
 | 
			
		||||
		if aNaf[i] > 0 {
 | 
			
		||||
			v.fromP1xP1(tmp1)
 | 
			
		||||
			aTable.SelectInto(multA, aNaf[i])
 | 
			
		||||
			tmp1.Add(v, multA)
 | 
			
		||||
		} else if aNaf[i] < 0 {
 | 
			
		||||
			v.fromP1xP1(tmp1)
 | 
			
		||||
			aTable.SelectInto(multA, -aNaf[i])
 | 
			
		||||
			tmp1.Sub(v, multA)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if bNaf[i] > 0 {
 | 
			
		||||
			v.fromP1xP1(tmp1)
 | 
			
		||||
			basepointNafTable.SelectInto(multB, bNaf[i])
 | 
			
		||||
			tmp1.AddAffine(v, multB)
 | 
			
		||||
		} else if bNaf[i] < 0 {
 | 
			
		||||
			v.fromP1xP1(tmp1)
 | 
			
		||||
			basepointNafTable.SelectInto(multB, -bNaf[i])
 | 
			
		||||
			tmp1.SubAffine(v, multB)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tmp2.FromP1xP1(tmp1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	v.fromP2(tmp2)
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										129
									
								
								vendor/filippo.io/edwards25519/tables.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								vendor/filippo.io/edwards25519/tables.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
			
		||||
// Copyright (c) 2019 The Go Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package edwards25519
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/subtle"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// A dynamic lookup table for variable-base, constant-time scalar muls.
 | 
			
		||||
type projLookupTable struct {
 | 
			
		||||
	points [8]projCached
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// A precomputed lookup table for fixed-base, constant-time scalar muls.
 | 
			
		||||
type affineLookupTable struct {
 | 
			
		||||
	points [8]affineCached
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// A dynamic lookup table for variable-base, variable-time scalar muls.
 | 
			
		||||
type nafLookupTable5 struct {
 | 
			
		||||
	points [8]projCached
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// A precomputed lookup table for fixed-base, variable-time scalar muls.
 | 
			
		||||
type nafLookupTable8 struct {
 | 
			
		||||
	points [64]affineCached
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Constructors.
 | 
			
		||||
 | 
			
		||||
// Builds a lookup table at runtime. Fast.
 | 
			
		||||
func (v *projLookupTable) FromP3(q *Point) {
 | 
			
		||||
	// Goal: v.points[i] = (i+1)*Q, i.e., Q, 2Q, ..., 8Q
 | 
			
		||||
	// This allows lookup of -8Q, ..., -Q, 0, Q, ..., 8Q
 | 
			
		||||
	v.points[0].FromP3(q)
 | 
			
		||||
	tmpP3 := Point{}
 | 
			
		||||
	tmpP1xP1 := projP1xP1{}
 | 
			
		||||
	for i := 0; i < 7; i++ {
 | 
			
		||||
		// Compute (i+1)*Q as Q + i*Q and convert to a projCached
 | 
			
		||||
		// This is needlessly complicated because the API has explicit
 | 
			
		||||
		// receivers instead of creating stack objects and relying on RVO
 | 
			
		||||
		v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.Add(q, &v.points[i])))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This is not optimised for speed; fixed-base tables should be precomputed.
 | 
			
		||||
func (v *affineLookupTable) FromP3(q *Point) {
 | 
			
		||||
	// Goal: v.points[i] = (i+1)*Q, i.e., Q, 2Q, ..., 8Q
 | 
			
		||||
	// This allows lookup of -8Q, ..., -Q, 0, Q, ..., 8Q
 | 
			
		||||
	v.points[0].FromP3(q)
 | 
			
		||||
	tmpP3 := Point{}
 | 
			
		||||
	tmpP1xP1 := projP1xP1{}
 | 
			
		||||
	for i := 0; i < 7; i++ {
 | 
			
		||||
		// Compute (i+1)*Q as Q + i*Q and convert to affineCached
 | 
			
		||||
		v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.AddAffine(q, &v.points[i])))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Builds a lookup table at runtime. Fast.
 | 
			
		||||
func (v *nafLookupTable5) FromP3(q *Point) {
 | 
			
		||||
	// Goal: v.points[i] = (2*i+1)*Q, i.e., Q, 3Q, 5Q, ..., 15Q
 | 
			
		||||
	// This allows lookup of -15Q, ..., -3Q, -Q, 0, Q, 3Q, ..., 15Q
 | 
			
		||||
	v.points[0].FromP3(q)
 | 
			
		||||
	q2 := Point{}
 | 
			
		||||
	q2.Add(q, q)
 | 
			
		||||
	tmpP3 := Point{}
 | 
			
		||||
	tmpP1xP1 := projP1xP1{}
 | 
			
		||||
	for i := 0; i < 7; i++ {
 | 
			
		||||
		v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.Add(&q2, &v.points[i])))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This is not optimised for speed; fixed-base tables should be precomputed.
 | 
			
		||||
func (v *nafLookupTable8) FromP3(q *Point) {
 | 
			
		||||
	v.points[0].FromP3(q)
 | 
			
		||||
	q2 := Point{}
 | 
			
		||||
	q2.Add(q, q)
 | 
			
		||||
	tmpP3 := Point{}
 | 
			
		||||
	tmpP1xP1 := projP1xP1{}
 | 
			
		||||
	for i := 0; i < 63; i++ {
 | 
			
		||||
		v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.AddAffine(&q2, &v.points[i])))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Selectors.
 | 
			
		||||
 | 
			
		||||
// Set dest to x*Q, where -8 <= x <= 8, in constant time.
 | 
			
		||||
func (v *projLookupTable) SelectInto(dest *projCached, x int8) {
 | 
			
		||||
	// Compute xabs = |x|
 | 
			
		||||
	xmask := x >> 7
 | 
			
		||||
	xabs := uint8((x + xmask) ^ xmask)
 | 
			
		||||
 | 
			
		||||
	dest.Zero()
 | 
			
		||||
	for j := 1; j <= 8; j++ {
 | 
			
		||||
		// Set dest = j*Q if |x| = j
 | 
			
		||||
		cond := subtle.ConstantTimeByteEq(xabs, uint8(j))
 | 
			
		||||
		dest.Select(&v.points[j-1], dest, cond)
 | 
			
		||||
	}
 | 
			
		||||
	// Now dest = |x|*Q, conditionally negate to get x*Q
 | 
			
		||||
	dest.CondNeg(int(xmask & 1))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set dest to x*Q, where -8 <= x <= 8, in constant time.
 | 
			
		||||
func (v *affineLookupTable) SelectInto(dest *affineCached, x int8) {
 | 
			
		||||
	// Compute xabs = |x|
 | 
			
		||||
	xmask := x >> 7
 | 
			
		||||
	xabs := uint8((x + xmask) ^ xmask)
 | 
			
		||||
 | 
			
		||||
	dest.Zero()
 | 
			
		||||
	for j := 1; j <= 8; j++ {
 | 
			
		||||
		// Set dest = j*Q if |x| = j
 | 
			
		||||
		cond := subtle.ConstantTimeByteEq(xabs, uint8(j))
 | 
			
		||||
		dest.Select(&v.points[j-1], dest, cond)
 | 
			
		||||
	}
 | 
			
		||||
	// Now dest = |x|*Q, conditionally negate to get x*Q
 | 
			
		||||
	dest.CondNeg(int(xmask & 1))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Given odd x with 0 < x < 2^4, return x*Q (in variable time).
 | 
			
		||||
func (v *nafLookupTable5) SelectInto(dest *projCached, x int8) {
 | 
			
		||||
	*dest = v.points[x/2]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Given odd x with 0 < x < 2^7, return x*Q (in variable time).
 | 
			
		||||
func (v *nafLookupTable8) SelectInto(dest *affineCached, x int8) {
 | 
			
		||||
	*dest = v.points[x/2]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								vendor/github.com/42wim/go-gitter/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								vendor/github.com/42wim/go-gitter/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,3 +0,0 @@
 | 
			
		||||
.idea
 | 
			
		||||
/test
 | 
			
		||||
app.yaml
 | 
			
		||||
							
								
								
									
										154
									
								
								vendor/github.com/42wim/go-gitter/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										154
									
								
								vendor/github.com/42wim/go-gitter/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,154 +0,0 @@
 | 
			
		||||
# gitter
 | 
			
		||||
Gitter API in Go
 | 
			
		||||
https://developer.gitter.im
 | 
			
		||||
 | 
			
		||||
#### Install
 | 
			
		||||
 | 
			
		||||
`go get github.com/sromku/go-gitter`
 | 
			
		||||
 | 
			
		||||
- [Initialize](#initialize)
 | 
			
		||||
- [Users](#users)
 | 
			
		||||
- [Rooms](#rooms)
 | 
			
		||||
- [Messages](#messages)
 | 
			
		||||
- [Stream](#stream)
 | 
			
		||||
- [Faye (Experimental)](#faye-experimental)
 | 
			
		||||
- [Debug](#debug)
 | 
			
		||||
- [App Engine](#app-engine)
 | 
			
		||||
 | 
			
		||||
##### Initialize
 | 
			
		||||
``` Go
 | 
			
		||||
api := gitter.New("YOUR_ACCESS_TOKEN")
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
##### Users
 | 
			
		||||
 | 
			
		||||
- Get current user
 | 
			
		||||
 | 
			
		||||
	``` Go
 | 
			
		||||
	user, err := api.GetUser()
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
##### Rooms
 | 
			
		||||
 | 
			
		||||
- Get all rooms
 | 
			
		||||
	``` Go
 | 
			
		||||
	rooms, err := api.GetRooms()
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
- Get room by id
 | 
			
		||||
	``` Go
 | 
			
		||||
	room, err := api.GetRoom("roomID")
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
- Get rooms of some user
 | 
			
		||||
	``` Go
 | 
			
		||||
	rooms, err := api.GetRooms("userID")
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
- Join room
 | 
			
		||||
	``` Go
 | 
			
		||||
	room, err := api.JoinRoom("roomID", "userID")
 | 
			
		||||
	```
 | 
			
		||||
	
 | 
			
		||||
- Leave room
 | 
			
		||||
	``` Go
 | 
			
		||||
	room, err := api.LeaveRoom("roomID", "userID")
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
- Get room id
 | 
			
		||||
	``` Go
 | 
			
		||||
	id, err := api.GetRoomId("room/uri")
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
- Search gitter rooms
 | 
			
		||||
	``` Go
 | 
			
		||||
	rooms, err := api.SearchRooms("search/string")
 | 
			
		||||
	```
 | 
			
		||||
##### Messages
 | 
			
		||||
 | 
			
		||||
- Get messages of room
 | 
			
		||||
	``` Go
 | 
			
		||||
	messages, err := api.GetMessages("roomID", nil)
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
- Get one message
 | 
			
		||||
	``` Go
 | 
			
		||||
	message, err := api.GetMessage("roomID", "messageID")
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
- Send message
 | 
			
		||||
	``` Go
 | 
			
		||||
	err := api.SendMessage("roomID", "free chat text")
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
##### Stream
 | 
			
		||||
 | 
			
		||||
Create stream to the room and start listening to incoming messages
 | 
			
		||||
 | 
			
		||||
``` Go
 | 
			
		||||
stream := api.Stream(room.Id)
 | 
			
		||||
go api.Listen(stream)
 | 
			
		||||
 | 
			
		||||
for {
 | 
			
		||||
    event := <-stream.Event
 | 
			
		||||
    switch ev := event.Data.(type) {
 | 
			
		||||
    case *gitter.MessageReceived:
 | 
			
		||||
        fmt.Println(ev.Message.From.Username + ": " + ev.Message.Text)
 | 
			
		||||
    case *gitter.GitterConnectionClosed:
 | 
			
		||||
        // connection was closed
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Close stream connection
 | 
			
		||||
 | 
			
		||||
``` Go
 | 
			
		||||
stream.Close()
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
##### Faye (Experimental)
 | 
			
		||||
 | 
			
		||||
``` Go
 | 
			
		||||
faye := api.Faye(room.ID)
 | 
			
		||||
go faye.Listen()
 | 
			
		||||
 | 
			
		||||
for {
 | 
			
		||||
    event := <-faye.Event
 | 
			
		||||
    switch ev := event.Data.(type) {
 | 
			
		||||
    case *gitter.MessageReceived:
 | 
			
		||||
        fmt.Println(ev.Message.From.Username + ": " + ev.Message.Text)
 | 
			
		||||
    case *gitter.GitterConnectionClosed: //this one is never called in Faye
 | 
			
		||||
        // connection was closed
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
##### Debug
 | 
			
		||||
 | 
			
		||||
You can print the internal errors by enabling debug to true
 | 
			
		||||
 | 
			
		||||
``` Go
 | 
			
		||||
api.SetDebug(true, nil)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You can also define your own `io.Writer` in case you want to persist the logs somewhere.
 | 
			
		||||
For example keeping the errors on file
 | 
			
		||||
 | 
			
		||||
``` Go
 | 
			
		||||
logFile, err := os.Create("gitter.log")
 | 
			
		||||
api.SetDebug(true, logFile)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
##### App Engine
 | 
			
		||||
 | 
			
		||||
Initialize app engine client and continue as usual
 | 
			
		||||
 | 
			
		||||
``` Go
 | 
			
		||||
c := appengine.NewContext(r)
 | 
			
		||||
client := urlfetch.Client(c)
 | 
			
		||||
 | 
			
		||||
api := gitter.New("YOUR_ACCESS_TOKEN")
 | 
			
		||||
api.SetClient(client)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
[Documentation](https://godoc.org/github.com/sromku/go-gitter)
 | 
			
		||||
							
								
								
									
										70
									
								
								vendor/github.com/42wim/go-gitter/faye.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										70
									
								
								vendor/github.com/42wim/go-gitter/faye.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,70 +0,0 @@
 | 
			
		||||
package gitter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/mrexodia/wray"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Faye struct {
 | 
			
		||||
	endpoint string
 | 
			
		||||
	Event    chan Event
 | 
			
		||||
	client   *wray.FayeClient
 | 
			
		||||
	gitter   *Gitter
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (gitter *Gitter) Faye(roomID string) *Faye {
 | 
			
		||||
	wray.RegisterTransports([]wray.Transport{
 | 
			
		||||
		&wray.HttpTransport{
 | 
			
		||||
			SendHook: func(data map[string]interface{}) {
 | 
			
		||||
				if channel, ok := data["channel"]; ok && channel == "/meta/handshake" {
 | 
			
		||||
					data["ext"] = map[string]interface{}{"token": gitter.config.token}
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	return &Faye{
 | 
			
		||||
		endpoint: "/api/v1/rooms/" + roomID + "/chatMessages",
 | 
			
		||||
		Event:    make(chan Event),
 | 
			
		||||
		client:   wray.NewFayeClient(fayeBaseURL),
 | 
			
		||||
		gitter:   gitter,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (faye *Faye) Listen() {
 | 
			
		||||
	defer faye.destroy()
 | 
			
		||||
 | 
			
		||||
	faye.client.Subscribe(faye.endpoint, false, func(message wray.Message) {
 | 
			
		||||
		dataBytes, err := json.Marshal(message.Data["model"])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Printf("JSON Marshal error: %v\n", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		var gitterMessage Message
 | 
			
		||||
		err = json.Unmarshal(dataBytes, &gitterMessage)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Printf("JSON Unmarshal error: %v\n", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		faye.Event <- Event{
 | 
			
		||||
			Data: &MessageReceived{
 | 
			
		||||
				Message: gitterMessage,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	//TODO: this might be needed in the future
 | 
			
		||||
	/*go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			faye.client.Publish("/api/v1/ping2", map[string]interface{}{"reason": "ping"})
 | 
			
		||||
			time.Sleep(60 * time.Second)
 | 
			
		||||
		}
 | 
			
		||||
	}()*/
 | 
			
		||||
 | 
			
		||||
	faye.client.Listen()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (faye *Faye) destroy() {
 | 
			
		||||
	close(faye.Event)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										527
									
								
								vendor/github.com/42wim/go-gitter/gitter.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										527
									
								
								vendor/github.com/42wim/go-gitter/gitter.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,527 +0,0 @@
 | 
			
		||||
// Package gitter is a Go client library for the Gitter API.
 | 
			
		||||
//
 | 
			
		||||
// Author: sromku
 | 
			
		||||
package gitter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/mreiferson/go-httpclient"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	apiBaseURL    = "https://api.gitter.im/v1/"
 | 
			
		||||
	streamBaseURL = "https://stream.gitter.im/v1/"
 | 
			
		||||
	fayeBaseURL   = "https://ws.gitter.im/faye"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Gitter struct {
 | 
			
		||||
	config struct {
 | 
			
		||||
		apiBaseURL    string
 | 
			
		||||
		streamBaseURL string
 | 
			
		||||
		token         string
 | 
			
		||||
		client        *http.Client
 | 
			
		||||
	}
 | 
			
		||||
	debug     bool
 | 
			
		||||
	logWriter io.Writer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New initializes the Gitter API client
 | 
			
		||||
//
 | 
			
		||||
// For example:
 | 
			
		||||
//  api := gitter.New("YOUR_ACCESS_TOKEN")
 | 
			
		||||
func New(token string) *Gitter {
 | 
			
		||||
 | 
			
		||||
	transport := &httpclient.Transport{
 | 
			
		||||
		ConnectTimeout:   5 * time.Second,
 | 
			
		||||
		ReadWriteTimeout: 40 * time.Second,
 | 
			
		||||
	}
 | 
			
		||||
	defer transport.Close()
 | 
			
		||||
 | 
			
		||||
	s := &Gitter{}
 | 
			
		||||
	s.config.apiBaseURL = apiBaseURL
 | 
			
		||||
	s.config.streamBaseURL = streamBaseURL
 | 
			
		||||
	s.config.token = token
 | 
			
		||||
	s.config.client = &http.Client{
 | 
			
		||||
		Transport: transport,
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetClient sets a custom http client. Can be useful in App Engine case.
 | 
			
		||||
func (gitter *Gitter) SetClient(client *http.Client) {
 | 
			
		||||
	gitter.config.client = client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUser returns the current user
 | 
			
		||||
func (gitter *Gitter) GetUser() (*User, error) {
 | 
			
		||||
 | 
			
		||||
	var users []User
 | 
			
		||||
	response, err := gitter.get(gitter.config.apiBaseURL + "user")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(response, &users)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(users) > 0 {
 | 
			
		||||
		return &users[0], nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = APIError{What: "Failed to retrieve current user"}
 | 
			
		||||
	gitter.log(err)
 | 
			
		||||
	return nil, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserRooms returns a list of Rooms the user is part of
 | 
			
		||||
func (gitter *Gitter) GetUserRooms(userID string) ([]Room, error) {
 | 
			
		||||
 | 
			
		||||
	var rooms []Room
 | 
			
		||||
	response, err := gitter.get(gitter.config.apiBaseURL + "user/" + userID + "/rooms")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(response, &rooms)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return rooms, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRooms returns a list of rooms the current user is in
 | 
			
		||||
func (gitter *Gitter) GetRooms() ([]Room, error) {
 | 
			
		||||
 | 
			
		||||
	var rooms []Room
 | 
			
		||||
	response, err := gitter.get(gitter.config.apiBaseURL + "rooms")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(response, &rooms)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return rooms, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUsersInRoom returns the users in the room with the passed id
 | 
			
		||||
func (gitter *Gitter) GetUsersInRoom(roomID string) ([]User, error) {
 | 
			
		||||
	var users []User
 | 
			
		||||
	response, err := gitter.get(gitter.config.apiBaseURL + "rooms/" + roomID + "/users")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(response, &users)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return users, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRoom returns a room with the passed id
 | 
			
		||||
func (gitter *Gitter) GetRoom(roomID string) (*Room, error) {
 | 
			
		||||
 | 
			
		||||
	var room Room
 | 
			
		||||
	response, err := gitter.get(gitter.config.apiBaseURL + "rooms/" + roomID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(response, &room)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &room, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetMessages returns a list of messages in a room.
 | 
			
		||||
// Pagination is optional. You can pass nil or specific pagination params.
 | 
			
		||||
func (gitter *Gitter) GetMessages(roomID string, params *Pagination) ([]Message, error) {
 | 
			
		||||
 | 
			
		||||
	var messages []Message
 | 
			
		||||
	url := gitter.config.apiBaseURL + "rooms/" + roomID + "/chatMessages"
 | 
			
		||||
	if params != nil {
 | 
			
		||||
		url += "?" + params.encode()
 | 
			
		||||
	}
 | 
			
		||||
	response, err := gitter.get(url)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(response, &messages)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return messages, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetMessage returns a message in a room.
 | 
			
		||||
func (gitter *Gitter) GetMessage(roomID, messageID string) (*Message, error) {
 | 
			
		||||
 | 
			
		||||
	var message Message
 | 
			
		||||
	response, err := gitter.get(gitter.config.apiBaseURL + "rooms/" + roomID + "/chatMessages/" + messageID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(response, &message)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &message, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendMessage sends a message to a room
 | 
			
		||||
func (gitter *Gitter) SendMessage(roomID, text string) (*Message, error) {
 | 
			
		||||
 | 
			
		||||
	message := Message{Text: text}
 | 
			
		||||
	body, _ := json.Marshal(message)
 | 
			
		||||
	response, err := gitter.post(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages", body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(response, &message)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &message, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateMessage updates a message in a room
 | 
			
		||||
func (gitter *Gitter) UpdateMessage(roomID, msgID, text string) (*Message, error) {
 | 
			
		||||
 | 
			
		||||
	message := Message{Text: text}
 | 
			
		||||
	body, _ := json.Marshal(message)
 | 
			
		||||
	response, err := gitter.put(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages/"+msgID, body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(response, &message)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &message, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JoinRoom joins a room
 | 
			
		||||
func (gitter *Gitter) JoinRoom(roomID, userID string) (*Room, error) {
 | 
			
		||||
 | 
			
		||||
	message := Room{ID: roomID}
 | 
			
		||||
	body, _ := json.Marshal(message)
 | 
			
		||||
	response, err := gitter.post(gitter.config.apiBaseURL+"user/"+userID+"/rooms", body)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var room Room
 | 
			
		||||
	err = json.Unmarshal(response, &room)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &room, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LeaveRoom removes a user from the room
 | 
			
		||||
func (gitter *Gitter) LeaveRoom(roomID, userID string) error {
 | 
			
		||||
 | 
			
		||||
	_, err := gitter.delete(gitter.config.apiBaseURL + "rooms/" + roomID + "/users/" + userID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetDebug traces errors if it's set to true.
 | 
			
		||||
func (gitter *Gitter) SetDebug(debug bool, logWriter io.Writer) {
 | 
			
		||||
	gitter.debug = debug
 | 
			
		||||
	gitter.logWriter = logWriter
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchRooms queries the Rooms resources of gitter API
 | 
			
		||||
func (gitter *Gitter) SearchRooms(room string) ([]Room, error) {
 | 
			
		||||
 | 
			
		||||
	var rooms struct {
 | 
			
		||||
		Results []Room `json:"results"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response, err := gitter.get(gitter.config.apiBaseURL + "rooms?q=" + room)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal(response, &rooms)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return rooms.Results, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRoomId returns the room ID of a given URI
 | 
			
		||||
func (gitter *Gitter) GetRoomId(uri string) (string, error) {
 | 
			
		||||
 | 
			
		||||
	rooms, err := gitter.SearchRooms(uri)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, element := range rooms {
 | 
			
		||||
		if element.URI == uri {
 | 
			
		||||
			return element.ID, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return "", APIError{What: "Room not found."}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Pagination params
 | 
			
		||||
type Pagination struct {
 | 
			
		||||
 | 
			
		||||
	// Skip n messages
 | 
			
		||||
	Skip int
 | 
			
		||||
 | 
			
		||||
	// Get messages before beforeId
 | 
			
		||||
	BeforeID string
 | 
			
		||||
 | 
			
		||||
	// Get messages after afterId
 | 
			
		||||
	AfterID string
 | 
			
		||||
 | 
			
		||||
	// Maximum number of messages to return
 | 
			
		||||
	Limit int
 | 
			
		||||
 | 
			
		||||
	// Search query
 | 
			
		||||
	Query string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (messageParams *Pagination) encode() string {
 | 
			
		||||
	values := url.Values{}
 | 
			
		||||
 | 
			
		||||
	if messageParams.AfterID != "" {
 | 
			
		||||
		values.Add("afterId", messageParams.AfterID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if messageParams.BeforeID != "" {
 | 
			
		||||
		values.Add("beforeId", messageParams.BeforeID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if messageParams.Skip > 0 {
 | 
			
		||||
		values.Add("skip", strconv.Itoa(messageParams.Skip))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if messageParams.Limit > 0 {
 | 
			
		||||
		values.Add("limit", strconv.Itoa(messageParams.Limit))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return values.Encode()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (gitter *Gitter) getResponse(url string, stream *Stream) (*http.Response, error) {
 | 
			
		||||
	r, err := http.NewRequest("GET", url, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	r.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	r.Header.Set("Accept", "application/json")
 | 
			
		||||
	r.Header.Set("Authorization", "Bearer "+gitter.config.token)
 | 
			
		||||
	if stream != nil {
 | 
			
		||||
		stream.streamConnection.request = r
 | 
			
		||||
	}
 | 
			
		||||
	response, err := gitter.config.client.Do(r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return response, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (gitter *Gitter) get(url string) ([]byte, error) {
 | 
			
		||||
	resp, err := gitter.getResponse(url, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return body, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (gitter *Gitter) post(url string, body []byte) ([]byte, error) {
 | 
			
		||||
	r, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	r.Header.Set("Accept", "application/json")
 | 
			
		||||
	r.Header.Set("Authorization", "Bearer "+gitter.config.token)
 | 
			
		||||
 | 
			
		||||
	resp, err := gitter.config.client.Do(r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (gitter *Gitter) put(url string, body []byte) ([]byte, error) {
 | 
			
		||||
	r, err := http.NewRequest("PUT", url, bytes.NewBuffer(body))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	r.Header.Set("Accept", "application/json")
 | 
			
		||||
	r.Header.Set("Authorization", "Bearer "+gitter.config.token)
 | 
			
		||||
 | 
			
		||||
	resp, err := gitter.config.client.Do(r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (gitter *Gitter) delete(url string) ([]byte, error) {
 | 
			
		||||
	r, err := http.NewRequest("delete", url, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	r.Header.Set("Accept", "application/json")
 | 
			
		||||
	r.Header.Set("Authorization", "Bearer "+gitter.config.token)
 | 
			
		||||
 | 
			
		||||
	resp, err := gitter.config.client.Do(r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		gitter.log(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (gitter *Gitter) log(a interface{}) {
 | 
			
		||||
	if gitter.debug {
 | 
			
		||||
		log.Println(a)
 | 
			
		||||
		if gitter.logWriter != nil {
 | 
			
		||||
			timestamp := time.Now().Format(time.RFC3339)
 | 
			
		||||
			msg := fmt.Sprintf("%v: %v", timestamp, a)
 | 
			
		||||
			fmt.Fprintln(gitter.logWriter, msg)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// APIError holds data of errors returned from the API.
 | 
			
		||||
type APIError struct {
 | 
			
		||||
	What string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e APIError) Error() string {
 | 
			
		||||
	return fmt.Sprintf("%v", e.What)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										142
									
								
								vendor/github.com/42wim/go-gitter/model.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										142
									
								
								vendor/github.com/42wim/go-gitter/model.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,142 +0,0 @@
 | 
			
		||||
package gitter
 | 
			
		||||
 | 
			
		||||
import "time"
 | 
			
		||||
 | 
			
		||||
// A Room in Gitter can represent a GitHub Organization, a GitHub Repository, a Gitter Channel or a One-to-one conversation.
 | 
			
		||||
// In the case of the Organizations and Repositories, the access control policies are inherited from GitHub.
 | 
			
		||||
type Room struct {
 | 
			
		||||
 | 
			
		||||
	// Room ID
 | 
			
		||||
	ID string `json:"id"`
 | 
			
		||||
 | 
			
		||||
	// Room name
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
 | 
			
		||||
	// Room topic. (default: GitHub repo description)
 | 
			
		||||
	Topic string `json:"topic"`
 | 
			
		||||
 | 
			
		||||
	// Room URI on Gitter
 | 
			
		||||
	URI string `json:"uri"`
 | 
			
		||||
 | 
			
		||||
	// Indicates if the room is a one-to-one chat
 | 
			
		||||
	OneToOne bool `json:"oneToOne"`
 | 
			
		||||
 | 
			
		||||
	// Count of users in the room
 | 
			
		||||
	UserCount int `json:"userCount"`
 | 
			
		||||
 | 
			
		||||
	// Number of unread messages for the current user
 | 
			
		||||
	UnreadItems int `json:"unreadItems"`
 | 
			
		||||
 | 
			
		||||
	// Number of unread mentions for the current user
 | 
			
		||||
	Mentions int `json:"mentions"`
 | 
			
		||||
 | 
			
		||||
	// Last time the current user accessed the room in ISO format
 | 
			
		||||
	LastAccessTime time.Time `json:"lastAccessTime"`
 | 
			
		||||
 | 
			
		||||
	// Indicates if the current user has disabled notifications
 | 
			
		||||
	Lurk bool `json:"lurk"`
 | 
			
		||||
 | 
			
		||||
	// Path to the room on gitter
 | 
			
		||||
	URL string `json:"url"`
 | 
			
		||||
 | 
			
		||||
	// Type of the room
 | 
			
		||||
	// - ORG: A room that represents a GitHub Organization.
 | 
			
		||||
	// - REPO: A room that represents a GitHub Repository.
 | 
			
		||||
	// - ONETOONE: A one-to-one chat.
 | 
			
		||||
	// - ORG_CHANNEL: A Gitter channel nested under a GitHub Organization.
 | 
			
		||||
	// - REPO_CHANNEL A Gitter channel nested under a GitHub Repository.
 | 
			
		||||
	// - USER_CHANNEL A Gitter channel nested under a GitHub User.
 | 
			
		||||
	GithubType string `json:"githubType"`
 | 
			
		||||
 | 
			
		||||
	// Tags that define the room
 | 
			
		||||
	Tags []string `json:"tags"`
 | 
			
		||||
 | 
			
		||||
	RoomMember bool `json:"roomMember"`
 | 
			
		||||
 | 
			
		||||
	// Room version.
 | 
			
		||||
	Version int `json:"v"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type User struct {
 | 
			
		||||
 | 
			
		||||
	// Gitter User ID
 | 
			
		||||
	ID string `json:"id"`
 | 
			
		||||
 | 
			
		||||
	// Gitter/GitHub username
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
 | 
			
		||||
	// Gitter/GitHub user real name
 | 
			
		||||
	DisplayName string `json:"displayName"`
 | 
			
		||||
 | 
			
		||||
	// Path to the user on Gitter
 | 
			
		||||
	URL string `json:"url"`
 | 
			
		||||
 | 
			
		||||
	// User avatar URI (small)
 | 
			
		||||
	AvatarURLSmall string `json:"avatarUrlSmall"`
 | 
			
		||||
 | 
			
		||||
	// User avatar URI (medium)
 | 
			
		||||
	AvatarURLMedium string `json:"avatarUrlMedium"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Message struct {
 | 
			
		||||
 | 
			
		||||
	// ID of the message
 | 
			
		||||
	ID string `json:"id"`
 | 
			
		||||
 | 
			
		||||
	// Original message in plain-text/markdown
 | 
			
		||||
	Text string `json:"text"`
 | 
			
		||||
 | 
			
		||||
	// HTML formatted message
 | 
			
		||||
	HTML string `json:"html"`
 | 
			
		||||
 | 
			
		||||
	// ISO formatted date of the message
 | 
			
		||||
	Sent time.Time `json:"sent"`
 | 
			
		||||
 | 
			
		||||
	// ISO formatted date of the message if edited
 | 
			
		||||
	EditedAt time.Time `json:"editedAt"`
 | 
			
		||||
 | 
			
		||||
	// User that sent the message
 | 
			
		||||
	From User `json:"fromUser"`
 | 
			
		||||
 | 
			
		||||
	// Boolean that indicates if the current user has read the message.
 | 
			
		||||
	Unread bool `json:"unread"`
 | 
			
		||||
 | 
			
		||||
	// Number of users that have read the message
 | 
			
		||||
	ReadBy int `json:"readBy"`
 | 
			
		||||
 | 
			
		||||
	// List of URLs present in the message
 | 
			
		||||
	Urls []URL `json:"urls"`
 | 
			
		||||
 | 
			
		||||
	// List of @Mentions in the message
 | 
			
		||||
	Mentions []Mention `json:"mentions"`
 | 
			
		||||
 | 
			
		||||
	// List of #Issues referenced in the message
 | 
			
		||||
	Issues []Issue `json:"issues"`
 | 
			
		||||
 | 
			
		||||
	// Version
 | 
			
		||||
	Version int `json:"v"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Mention holds data about mentioned user in the message
 | 
			
		||||
type Mention struct {
 | 
			
		||||
 | 
			
		||||
	// User's username
 | 
			
		||||
	ScreenName string `json:"screenName"`
 | 
			
		||||
 | 
			
		||||
	// Gitter User ID
 | 
			
		||||
	UserID string `json:"userID"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Issue references issue in the message
 | 
			
		||||
type Issue struct {
 | 
			
		||||
 | 
			
		||||
	// Issue number
 | 
			
		||||
	Number string `json:"number"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// URL presented in the message
 | 
			
		||||
type URL struct {
 | 
			
		||||
 | 
			
		||||
	// URL
 | 
			
		||||
	URL string `json:"url"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										217
									
								
								vendor/github.com/42wim/go-gitter/stream.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										217
									
								
								vendor/github.com/42wim/go-gitter/stream.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,217 +0,0 @@
 | 
			
		||||
package gitter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/mreiferson/go-httpclient"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var defaultConnectionWaitTime time.Duration = 3000 // millis
 | 
			
		||||
var defaultConnectionMaxRetries = 5
 | 
			
		||||
 | 
			
		||||
// Stream initialize stream
 | 
			
		||||
func (gitter *Gitter) Stream(roomID string) *Stream {
 | 
			
		||||
	return &Stream{
 | 
			
		||||
		url:    streamBaseURL + "rooms/" + roomID + "/chatMessages",
 | 
			
		||||
		Event:  make(chan Event),
 | 
			
		||||
		gitter: gitter,
 | 
			
		||||
		streamConnection: gitter.newStreamConnection(
 | 
			
		||||
			defaultConnectionWaitTime,
 | 
			
		||||
			defaultConnectionMaxRetries),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Implemented to conform with https://developer.gitter.im/docs/streaming-api
 | 
			
		||||
func (gitter *Gitter) Listen(stream *Stream) {
 | 
			
		||||
 | 
			
		||||
	defer stream.destroy()
 | 
			
		||||
 | 
			
		||||
	var reader *bufio.Reader
 | 
			
		||||
	var gitterMessage Message
 | 
			
		||||
	lastKeepalive := time.Now().Unix()
 | 
			
		||||
 | 
			
		||||
	// connect
 | 
			
		||||
	stream.connect()
 | 
			
		||||
 | 
			
		||||
Loop:
 | 
			
		||||
	for {
 | 
			
		||||
 | 
			
		||||
		// if closed then stop trying
 | 
			
		||||
		if stream.isClosed() {
 | 
			
		||||
			stream.Event <- Event{
 | 
			
		||||
				Data: &GitterConnectionClosed{},
 | 
			
		||||
			}
 | 
			
		||||
			break Loop
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		resp := stream.getResponse()
 | 
			
		||||
		if resp.StatusCode != 200 {
 | 
			
		||||
			gitter.log(fmt.Sprintf("Unexpected response code %v", resp.StatusCode))
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		//"The JSON stream returns messages as JSON objects that are delimited by carriage return (\r)" <- Not true crap it's (\n) only
 | 
			
		||||
		reader = bufio.NewReader(resp.Body)
 | 
			
		||||
		line, err := reader.ReadBytes('\n')
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			gitter.log("ReadBytes error: " + err.Error())
 | 
			
		||||
			stream.connect()
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//Check if the line only consists of whitespace
 | 
			
		||||
		onlyWhitespace := true
 | 
			
		||||
		for _, b := range line {
 | 
			
		||||
			if b != ' ' && b != '\t' && b != '\r' && b != '\n' {
 | 
			
		||||
				onlyWhitespace = false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if onlyWhitespace {
 | 
			
		||||
			//"Parsers must be tolerant of occasional extra newline characters placed between messages."
 | 
			
		||||
			currentKeepalive := time.Now().Unix() //interesting behavior of 100+ keepalives per seconds was observed
 | 
			
		||||
			if currentKeepalive-lastKeepalive > 10 {
 | 
			
		||||
				lastKeepalive = currentKeepalive
 | 
			
		||||
				gitter.log("Keepalive was received")
 | 
			
		||||
			}
 | 
			
		||||
			continue
 | 
			
		||||
		} else if stream.isClosed() {
 | 
			
		||||
			gitter.log("Stream closed")
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// unmarshal the streamed data
 | 
			
		||||
		err = json.Unmarshal(line, &gitterMessage)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			gitter.log("JSON Unmarshal error: " + err.Error())
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// we are here, then we got the good message. pipe it forward.
 | 
			
		||||
		stream.Event <- Event{
 | 
			
		||||
			Data: &MessageReceived{
 | 
			
		||||
				Message: gitterMessage,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gitter.log("Listening was completed")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Stream holds stream data.
 | 
			
		||||
type Stream struct {
 | 
			
		||||
	url              string
 | 
			
		||||
	Event            chan Event
 | 
			
		||||
	streamConnection *streamConnection
 | 
			
		||||
	gitter           *Gitter
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (stream *Stream) destroy() {
 | 
			
		||||
	close(stream.Event)
 | 
			
		||||
	stream.streamConnection.currentRetries = 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Event struct {
 | 
			
		||||
	Data interface{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GitterConnectionClosed struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MessageReceived struct {
 | 
			
		||||
	Message Message
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// connect and try to reconnect with
 | 
			
		||||
func (stream *Stream) connect() {
 | 
			
		||||
 | 
			
		||||
	if stream.streamConnection.retries == stream.streamConnection.currentRetries {
 | 
			
		||||
		stream.Close()
 | 
			
		||||
		stream.gitter.log("Number of retries exceeded the max retries number, we are done here")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res, err := stream.gitter.getResponse(stream.url, stream)
 | 
			
		||||
	if err != nil || res.StatusCode != 200 {
 | 
			
		||||
		stream.gitter.log("Failed to get response, trying reconnect")
 | 
			
		||||
		if res != nil {
 | 
			
		||||
			stream.gitter.log(fmt.Sprintf("Status code: %v", res.StatusCode))
 | 
			
		||||
		}
 | 
			
		||||
		stream.gitter.log(err)
 | 
			
		||||
 | 
			
		||||
		// sleep and wait
 | 
			
		||||
		stream.streamConnection.currentRetries++
 | 
			
		||||
		time.Sleep(time.Millisecond * stream.streamConnection.wait * time.Duration(stream.streamConnection.currentRetries))
 | 
			
		||||
 | 
			
		||||
		// connect again
 | 
			
		||||
		stream.Close()
 | 
			
		||||
		stream.connect()
 | 
			
		||||
	} else {
 | 
			
		||||
		stream.gitter.log("Response was received")
 | 
			
		||||
		stream.streamConnection.currentRetries = 0
 | 
			
		||||
		stream.streamConnection.closed = false
 | 
			
		||||
		stream.streamConnection.response = res
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type streamConnection struct {
 | 
			
		||||
 | 
			
		||||
	// connection was closed
 | 
			
		||||
	closed bool
 | 
			
		||||
 | 
			
		||||
	// wait time till next try
 | 
			
		||||
	wait time.Duration
 | 
			
		||||
 | 
			
		||||
	// max tries to recover
 | 
			
		||||
	retries int
 | 
			
		||||
 | 
			
		||||
	// current streamed response
 | 
			
		||||
	response *http.Response
 | 
			
		||||
 | 
			
		||||
	// current request
 | 
			
		||||
	request *http.Request
 | 
			
		||||
 | 
			
		||||
	// current status
 | 
			
		||||
	currentRetries int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Close the stream connection and stop receiving streamed data
 | 
			
		||||
func (stream *Stream) Close() {
 | 
			
		||||
	conn := stream.streamConnection
 | 
			
		||||
	conn.closed = true
 | 
			
		||||
	if conn.response != nil {
 | 
			
		||||
		stream.gitter.log("Stream connection close response")
 | 
			
		||||
		defer conn.response.Body.Close()
 | 
			
		||||
	}
 | 
			
		||||
	if conn.request != nil {
 | 
			
		||||
		stream.gitter.log("Stream connection close request")
 | 
			
		||||
		switch transport := stream.gitter.config.client.Transport.(type) {
 | 
			
		||||
		case *httpclient.Transport:
 | 
			
		||||
			transport.CancelRequest(conn.request)
 | 
			
		||||
		default:
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (stream *Stream) isClosed() bool {
 | 
			
		||||
	return stream.streamConnection.closed
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (stream *Stream) getResponse() *http.Response {
 | 
			
		||||
	return stream.streamConnection.response
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Optional, set stream connection properties
 | 
			
		||||
// wait - time in milliseconds of waiting between reconnections. Will grow exponentially.
 | 
			
		||||
// retries - number of reconnections retries before dropping the stream.
 | 
			
		||||
func (gitter *Gitter) newStreamConnection(wait time.Duration, retries int) *streamConnection {
 | 
			
		||||
	return &streamConnection{
 | 
			
		||||
		closed:  true,
 | 
			
		||||
		wait:    wait,
 | 
			
		||||
		retries: retries,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								vendor/github.com/42wim/go-gitter/test_utils.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								vendor/github.com/42wim/go-gitter/test_utils.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,30 +0,0 @@
 | 
			
		||||
package gitter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"net/url"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	mux    *http.ServeMux
 | 
			
		||||
	gitter *Gitter
 | 
			
		||||
	server *httptest.Server
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func setup() {
 | 
			
		||||
	mux = http.NewServeMux()
 | 
			
		||||
	server = httptest.NewServer(mux)
 | 
			
		||||
 | 
			
		||||
	gitter = New("abc")
 | 
			
		||||
 | 
			
		||||
	// Fake the API and Stream base URLs by using the test
 | 
			
		||||
	// server URL instead.
 | 
			
		||||
	url, _ := url.Parse(server.URL)
 | 
			
		||||
	gitter.config.apiBaseURL = url.String() + "/"
 | 
			
		||||
	gitter.config.streamBaseURL = url.String() + "/"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func teardown() {
 | 
			
		||||
	server.Close()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								vendor/github.com/Rhymen/go-whatsapp/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/github.com/Rhymen/go-whatsapp/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -70,7 +70,7 @@ func (myHandler) HandleContactMessage(message whatsapp.ContactMessage) {
 | 
			
		||||
	fmt.Println(message)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (myHandler) HandleBatteryMessage(msg whatsapp.BatteryMessage) {
 | 
			
		||||
func (myHandler) HandleBatteryMessage(message whatsapp.BatteryMessage) {
 | 
			
		||||
	fmt.Println(message)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								vendor/github.com/Rhymen/go-whatsapp/session.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								vendor/github.com/Rhymen/go-whatsapp/session.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -18,7 +18,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
//represents the WhatsAppWeb client version
 | 
			
		||||
var waVersion = []int{2, 2121, 6}
 | 
			
		||||
var waVersion = []int{2, 2142, 12}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Session contains session individual information. To be able to resume the connection without scanning the qr code
 | 
			
		||||
@@ -526,5 +526,7 @@ func (wac *Conn) Logout() error {
 | 
			
		||||
		return fmt.Errorf("error writing logout: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wac.loggedIn = false
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user