forked from lug/matterbridge
		
	Compare commits
	
		
			11 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6d8cccccf1 | ||
| 
						 | 
					dd63438e7b | ||
| 
						 | 
					5f5d6c6e8a | ||
| 
						 | 
					aefa8a9341 | ||
| 
						 | 
					1c3e764d57 | ||
| 
						 | 
					0b03076a9d | ||
| 
						 | 
					1e6a2bc8f7 | ||
| 
						 | 
					82fe80e52f | ||
| 
						 | 
					db012bd9b7 | ||
| 
						 | 
					dd2374158b | ||
| 
						 | 
					6693157258 | 
@@ -1,2 +0,0 @@
 | 
			
		||||
Dockerfile
 | 
			
		||||
tgs.Dockerfile
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
go:
 | 
			
		||||
  comments:
 | 
			
		||||
    disabled: true
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/Bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/Bug_report.md
									
									
									
									
										vendored
									
									
								
							@@ -1,7 +1,6 @@
 | 
			
		||||
---
 | 
			
		||||
name: Bug report
 | 
			
		||||
about: Create a report to help us improve. (Check the FAQ on the wiki first)
 | 
			
		||||
labels: bug
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/Feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/Feature_request.md
									
									
									
									
										vendored
									
									
								
							@@ -1,7 +1,6 @@
 | 
			
		||||
---
 | 
			
		||||
name: Feature request
 | 
			
		||||
about: Suggest an idea for this project
 | 
			
		||||
labels: enhancement
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										71
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										71
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,71 +0,0 @@
 | 
			
		||||
# For most projects, this workflow file will not need changing; you simply need
 | 
			
		||||
# to commit it to your repository.
 | 
			
		||||
#
 | 
			
		||||
# You may wish to alter this file to override the set of languages analyzed,
 | 
			
		||||
# or to provide custom queries or build logic.
 | 
			
		||||
name: "CodeQL"
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [master]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    # The branches below must be a subset of the branches above
 | 
			
		||||
    branches: [master]
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: '0 16 * * 1'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  analyze:
 | 
			
		||||
    name: Analyze
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        # Override automatic language detection by changing the below list
 | 
			
		||||
        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
 | 
			
		||||
        language: ['go']
 | 
			
		||||
        # Learn more...
 | 
			
		||||
        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Checkout repository
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
      with:
 | 
			
		||||
        # We must fetch at least the immediate parents so that if this is
 | 
			
		||||
        # a pull request then we can checkout the head.
 | 
			
		||||
        fetch-depth: 2
 | 
			
		||||
 | 
			
		||||
    # If this run was triggered by a pull request event, then checkout
 | 
			
		||||
    # the head of the pull request instead of the merge commit.
 | 
			
		||||
    - run: git checkout HEAD^2
 | 
			
		||||
      if: ${{ github.event_name == 'pull_request' }}
 | 
			
		||||
 | 
			
		||||
    # Initializes the CodeQL tools for scanning.
 | 
			
		||||
    - name: Initialize CodeQL
 | 
			
		||||
      uses: github/codeql-action/init@v1
 | 
			
		||||
      with:
 | 
			
		||||
        languages: ${{ matrix.language }}
 | 
			
		||||
        # If you wish to specify custom queries, you can do so here or in a config file.
 | 
			
		||||
        # By default, queries listed here will override any specified in a config file. 
 | 
			
		||||
        # Prefix the list here with "+" to use these queries and those in the config file.
 | 
			
		||||
        # queries: ./path/to/local/query, your-org/your-repo/queries@main
 | 
			
		||||
 | 
			
		||||
    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
 | 
			
		||||
    # If this step fails, then you should remove it and run the build manually (see below)
 | 
			
		||||
    - name: Autobuild
 | 
			
		||||
      uses: github/codeql-action/autobuild@v1
 | 
			
		||||
 | 
			
		||||
    # ℹ️ Command-line programs to run using the OS shell.
 | 
			
		||||
    # 📚 https://git.io/JvXDl
 | 
			
		||||
 | 
			
		||||
    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
 | 
			
		||||
    #    and modify them (or add more) to build your code if your project
 | 
			
		||||
    #    uses a compiled language
 | 
			
		||||
 | 
			
		||||
    #- run: |
 | 
			
		||||
    #   make bootstrap
 | 
			
		||||
    #   make release
 | 
			
		||||
 | 
			
		||||
    - name: Perform CodeQL Analysis
 | 
			
		||||
      uses: github/codeql-action/analyze@v1
 | 
			
		||||
							
								
								
									
										58
									
								
								.github/workflows/development.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										58
									
								
								.github/workflows/development.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,58 +0,0 @@
 | 
			
		||||
name: Development
 | 
			
		||||
on: [push, pull_request]
 | 
			
		||||
jobs:
 | 
			
		||||
  lint:
 | 
			
		||||
    name: golangci-lint
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 20
 | 
			
		||||
      - name: Run golangci-lint
 | 
			
		||||
        uses: golangci/golangci-lint-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          version: latest
 | 
			
		||||
          args: "-v --new-from-rev HEAD~5"
 | 
			
		||||
  test-build-upload:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        go-version: [1.18.x]
 | 
			
		||||
        platform: [ubuntu-latest]
 | 
			
		||||
    runs-on: ${{ matrix.platform }}
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Install Go
 | 
			
		||||
      uses: actions/setup-go@v2
 | 
			
		||||
      with:
 | 
			
		||||
        go-version: ${{ matrix.go-version }}
 | 
			
		||||
        stable: false
 | 
			
		||||
    - name: Checkout code
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
      with:
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
    - name: Test
 | 
			
		||||
      run: go test ./... -mod=vendor
 | 
			
		||||
    - name: Build
 | 
			
		||||
      run: |
 | 
			
		||||
        mkdir -p output/{win,lin,arm,mac}
 | 
			
		||||
        VERSION=$(git describe --tags)
 | 
			
		||||
        CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64
 | 
			
		||||
        CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe
 | 
			
		||||
        CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64
 | 
			
		||||
    - name: Upload linux 64-bit
 | 
			
		||||
      if: startsWith(matrix.go-version,'1.18')
 | 
			
		||||
      uses: actions/upload-artifact@v2
 | 
			
		||||
      with:
 | 
			
		||||
        name: matterbridge-linux-64bit
 | 
			
		||||
        path: output/lin
 | 
			
		||||
    - name: Upload windows 64-bit
 | 
			
		||||
      if: startsWith(matrix.go-version,'1.18')
 | 
			
		||||
      uses: actions/upload-artifact@v2
 | 
			
		||||
      with:
 | 
			
		||||
        name: matterbridge-windows-64bit
 | 
			
		||||
        path: output/win
 | 
			
		||||
    - name: Upload darwin 64-bit
 | 
			
		||||
      if: startsWith(matrix.go-version,'1.18')
 | 
			
		||||
      uses: actions/upload-artifact@v2
 | 
			
		||||
      with:
 | 
			
		||||
        name: matterbridge-darwin-64bit
 | 
			
		||||
        path: output/mac
 | 
			
		||||
							
								
								
									
										68
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										68
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,68 +0,0 @@
 | 
			
		||||
name: docker
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - 'master'
 | 
			
		||||
    tags:
 | 
			
		||||
      - 'v*'
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches:
 | 
			
		||||
      - 'master'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  docker:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      -
 | 
			
		||||
        name: Checkout
 | 
			
		||||
        uses: actions/checkout@v2
 | 
			
		||||
      -
 | 
			
		||||
        name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          platforms: amd64,arm64
 | 
			
		||||
      -
 | 
			
		||||
        name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v1
 | 
			
		||||
      -
 | 
			
		||||
        name: Docker meta
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          images: 42wim/matterbridge,ghcr.io/42wim/matterbridge
 | 
			
		||||
          flavor: |
 | 
			
		||||
            latest=true
 | 
			
		||||
          tags: |
 | 
			
		||||
            type=ref,event=branch
 | 
			
		||||
            type=ref,event=pr
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
            type=semver,pattern=stable
 | 
			
		||||
            type=semver,pattern={{major}}
 | 
			
		||||
            type=semver,pattern={{major}}.{{minor}}
 | 
			
		||||
      -
 | 
			
		||||
        name: Login to DockerHub
 | 
			
		||||
        uses: docker/login-action@v1 
 | 
			
		||||
        if: github.event_name != 'pull_request'
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
      -
 | 
			
		||||
        name: Log into registry ghcr.io
 | 
			
		||||
        uses: docker/login-action@v1
 | 
			
		||||
        if: github.event_name != 'pull_request'
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.repository_owner }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
      -
 | 
			
		||||
        name: Build and push
 | 
			
		||||
        id: docker_build
 | 
			
		||||
        uses: docker/build-push-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          push: ${{ github.event_name != 'pull_request' }}
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,9 +0,0 @@
 | 
			
		||||
# Exclude matterbridge binary
 | 
			
		||||
/matterbridge
 | 
			
		||||
/matterbridge.exe
 | 
			
		||||
 | 
			
		||||
# Exclude configuration file
 | 
			
		||||
matterbridge.toml
 | 
			
		||||
 | 
			
		||||
# Exclude IDE Files
 | 
			
		||||
.vscode
 | 
			
		||||
@@ -7,7 +7,7 @@ run:
 | 
			
		||||
  # concurrency: 4
 | 
			
		||||
 | 
			
		||||
  # timeout for analysis, e.g. 30s, 5m, default is 1m
 | 
			
		||||
  deadline: 5m
 | 
			
		||||
  deadline: 2m
 | 
			
		||||
 | 
			
		||||
  # exit code when at least one issue was found, default is 1
 | 
			
		||||
  issues-exit-code: 1
 | 
			
		||||
@@ -23,7 +23,7 @@ run:
 | 
			
		||||
  # default value is empty list, but next dirs are always skipped independently
 | 
			
		||||
  # from this option's value:
 | 
			
		||||
  #   	vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
 | 
			
		||||
  skip-dirs: gateway/bridgemap$
 | 
			
		||||
  skip-dirs:
 | 
			
		||||
 | 
			
		||||
  # which files to skip: they will be analyzed, but issues from them
 | 
			
		||||
  # won't be reported. Default value is empty list, but there is
 | 
			
		||||
@@ -91,6 +91,7 @@ linters-settings:
 | 
			
		||||
    # Correct spellings using locale preferences for US or UK.
 | 
			
		||||
    # Default is to use a neutral variety of English.
 | 
			
		||||
    # Setting locale to US will correct the British spelling of 'colour' to 'color'.
 | 
			
		||||
    locale: US
 | 
			
		||||
  lll:
 | 
			
		||||
    # max line length, lines longer will be reported. Default is 120.
 | 
			
		||||
    # '\t' is counted as 1 character by default, and can be changed with the tab-width option
 | 
			
		||||
@@ -173,45 +174,8 @@ linters:
 | 
			
		||||
    - lll
 | 
			
		||||
    - maligned
 | 
			
		||||
    - prealloc
 | 
			
		||||
    - wsl
 | 
			
		||||
    - gomnd
 | 
			
		||||
    - godox
 | 
			
		||||
    - goerr113
 | 
			
		||||
    - testpackage
 | 
			
		||||
    - godot
 | 
			
		||||
    - interfacer
 | 
			
		||||
    - goheader
 | 
			
		||||
    - noctx
 | 
			
		||||
    - gci
 | 
			
		||||
    - errorlint
 | 
			
		||||
    - nlreturn
 | 
			
		||||
    - exhaustivestruct
 | 
			
		||||
    - forbidigo
 | 
			
		||||
    - wrapcheck
 | 
			
		||||
    - varnamelen
 | 
			
		||||
    - ireturn
 | 
			
		||||
    - errorlint
 | 
			
		||||
    - tparallel
 | 
			
		||||
    - wrapcheck
 | 
			
		||||
    - paralleltest
 | 
			
		||||
    - makezero
 | 
			
		||||
    - thelper
 | 
			
		||||
    - cyclop
 | 
			
		||||
    - revive
 | 
			
		||||
    - importas
 | 
			
		||||
    - gomoddirectives
 | 
			
		||||
    - promlinter
 | 
			
		||||
    - tagliatelle
 | 
			
		||||
    - errname
 | 
			
		||||
    - typecheck
 | 
			
		||||
    - grouper
 | 
			
		||||
    - decorder
 | 
			
		||||
    - maintidx
 | 
			
		||||
    - exhaustruct
 | 
			
		||||
    - asasalint
 | 
			
		||||
    - execinquery
 | 
			
		||||
    - nosnakecase
 | 
			
		||||
    - exhaustive
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# rules to deal with reported isues
 | 
			
		||||
issues:
 | 
			
		||||
  # List of regexps of issue texts to exclude, empty list by default.
 | 
			
		||||
 
 | 
			
		||||
@@ -18,24 +18,17 @@ builds:
 | 
			
		||||
    - arm
 | 
			
		||||
    - arm64
 | 
			
		||||
    - 386
 | 
			
		||||
  goarm:
 | 
			
		||||
    - 6
 | 
			
		||||
    - 7
 | 
			
		||||
  ldflags:
 | 
			
		||||
    - -s -w -X github.com/42wim/matterbridge/version.GitHash={{.ShortCommit}}
 | 
			
		||||
    - -s -w -X main.githash={{.ShortCommit}}
 | 
			
		||||
 | 
			
		||||
archives:
 | 
			
		||||
  -
 | 
			
		||||
    id: matterbridge
 | 
			
		||||
    builds:
 | 
			
		||||
    - matterbridge
 | 
			
		||||
    name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
 | 
			
		||||
    format: binary
 | 
			
		||||
    files:
 | 
			
		||||
      - none*
 | 
			
		||||
    replacements:
 | 
			
		||||
      386: 32bit
 | 
			
		||||
      amd64: 64bit
 | 
			
		||||
archive:
 | 
			
		||||
  name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
 | 
			
		||||
  format: binary
 | 
			
		||||
  files:
 | 
			
		||||
    - none*
 | 
			
		||||
  replacements:
 | 
			
		||||
    386: 32bit
 | 
			
		||||
    amd64: 64bit
 | 
			
		||||
 | 
			
		||||
checksum:
 | 
			
		||||
  name_template: 'checksums.txt'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
language: go
 | 
			
		||||
go_import_path: github.com/42wim/matterbridge
 | 
			
		||||
 | 
			
		||||
# We have everything vendored so this helps TravisCI not run `go get ...`.
 | 
			
		||||
install: true
 | 
			
		||||
 | 
			
		||||
git:
 | 
			
		||||
  depth: 200
 | 
			
		||||
 | 
			
		||||
notifications:
 | 
			
		||||
  email: false
 | 
			
		||||
 | 
			
		||||
branches:
 | 
			
		||||
  only:
 | 
			
		||||
  - master
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  include:
 | 
			
		||||
  - stage: lint
 | 
			
		||||
    # Run linting in one Go environment only.
 | 
			
		||||
    script: ./ci/lint.sh
 | 
			
		||||
    go: 1.12.x
 | 
			
		||||
    env:
 | 
			
		||||
    - GO111MODULE=on
 | 
			
		||||
    - GOLANGCI_VERSION="v1.16.0"
 | 
			
		||||
  - stage: test
 | 
			
		||||
    # Run tests in a combination of Go environments.
 | 
			
		||||
    script: ./ci/test.sh
 | 
			
		||||
    go: 1.11.x
 | 
			
		||||
    env:
 | 
			
		||||
    - GO111MODULE=off
 | 
			
		||||
  - script: ./ci/test.sh
 | 
			
		||||
    go: 1.11.x
 | 
			
		||||
    env:
 | 
			
		||||
    - GO111MODULE=on
 | 
			
		||||
  - script: ./ci/test.sh
 | 
			
		||||
    go: 1.12.x
 | 
			
		||||
    env:
 | 
			
		||||
    - GO111MODULE=on
 | 
			
		||||
    - REPORT_COVERAGE=1
 | 
			
		||||
    - BINDEPLOY=1
 | 
			
		||||
 | 
			
		||||
before_deploy: /bin/bash ci/bintray.sh
 | 
			
		||||
 | 
			
		||||
deploy:
 | 
			
		||||
  on:
 | 
			
		||||
    all_branches: true
 | 
			
		||||
    condition: $BINDEPLOY = 1
 | 
			
		||||
  provider: bintray
 | 
			
		||||
  edge:
 | 
			
		||||
    branch: v1.8.47
 | 
			
		||||
  file: ci/deploy.json
 | 
			
		||||
  user: 42wim
 | 
			
		||||
  key:
 | 
			
		||||
    secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="
 | 
			
		||||
							
								
								
									
										23
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,14 +1,11 @@
 | 
			
		||||
FROM alpine AS builder
 | 
			
		||||
FROM alpine:edge
 | 
			
		||||
ENTRYPOINT ["/bin/matterbridge"]
 | 
			
		||||
 | 
			
		||||
COPY . /go/src/matterbridge
 | 
			
		||||
RUN apk --no-cache add go git \
 | 
			
		||||
        && cd /go/src/matterbridge \
 | 
			
		||||
        && CGO_ENABLED=0 go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
 | 
			
		||||
 | 
			
		||||
FROM alpine
 | 
			
		||||
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"]
 | 
			
		||||
COPY . /go/src/github.com/42wim/matterbridge
 | 
			
		||||
RUN apk update && apk add go git gcc musl-dev ca-certificates \
 | 
			
		||||
        && cd /go/src/github.com/42wim/matterbridge \
 | 
			
		||||
        && export GOPATH=/go \
 | 
			
		||||
        && go get \
 | 
			
		||||
        && go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge \
 | 
			
		||||
        && rm -rf /go \
 | 
			
		||||
        && apk del --purge git go gcc musl-dev
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
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"]
 | 
			
		||||
							
								
								
									
										442
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										442
									
								
								README.md
									
									
									
									
									
								
							@@ -3,35 +3,32 @@
 | 
			
		||||
# matterbridge
 | 
			
		||||
 | 
			
		||||
<br />
 | 
			
		||||
**A simple chat bridge**<br />
 | 
			
		||||
Letting people be where they want to be.<br />
 | 
			
		||||
<sub>Bridges between a growing number of protocols. Click below to demo or join the development chat.</sub>
 | 
			
		||||
   **A simple chat bridge**<br />
 | 
			
		||||
   Letting people be where they want to be.<br />
 | 
			
		||||
   <sub>Bridges between a growing number of protocols. Click below to demo or join the development chat.</sub>
 | 
			
		||||
 | 
			
		||||
   <sup>
 | 
			
		||||
 | 
			
		||||
[Discord][mb-discord] |
 | 
			
		||||
[Gitter][mb-gitter] |
 | 
			
		||||
[IRC][mb-irc] |
 | 
			
		||||
[Keybase][mb-keybase] |
 | 
			
		||||
[Matrix][mb-matrix] |
 | 
			
		||||
[Mattermost][mb-mattermost] |
 | 
			
		||||
[MSTeams][mb-msteams] |
 | 
			
		||||
[Rocket.Chat][mb-rocketchat] |
 | 
			
		||||
[Slack][mb-slack] |
 | 
			
		||||
[Telegram][mb-telegram] |
 | 
			
		||||
[Twitch][mb-twitch] |
 | 
			
		||||
[WhatsApp][mb-whatsapp] |
 | 
			
		||||
[XMPP][mb-xmpp] |
 | 
			
		||||
[Zulip][mb-zulip] |
 | 
			
		||||
And more...
 | 
			
		||||
</sup>
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
   [Gitter][mb-gitter] |
 | 
			
		||||
   [IRC][mb-irc] |
 | 
			
		||||
      [Discord][mb-discord] |
 | 
			
		||||
      [Matrix][mb-matrix] |
 | 
			
		||||
      [Slack][mb-slack] |
 | 
			
		||||
      [Mattermost][mb-mattermost] |
 | 
			
		||||
      [Rocket.Chat][mb-rocketchat] |
 | 
			
		||||
      [XMPP][mb-xmpp] |
 | 
			
		||||
      [Twitch][mb-twitch] |
 | 
			
		||||
      [WhatsApp][mb-whatsapp] |
 | 
			
		||||
      [Zulip][mb-zulip] |
 | 
			
		||||
      [Telegram][mb-telegram] |
 | 
			
		||||
      And more...
 | 
			
		||||
   </sup>
 | 
			
		||||
 | 
			
		||||
----
 | 
			
		||||
[](https://github.com/42wim/matterbridge/releases/latest)
 | 
			
		||||
[](https://codeclimate.com/github/42wim/matterbridge/maintainability)
 | 
			
		||||
[](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br />
 | 
			
		||||
 | 
			
		||||
   [](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
 | 
			
		||||
   [](https://codeclimate.com/github/42wim/matterbridge/maintainability)
 | 
			
		||||
   [](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br />
 | 
			
		||||
  <hr />
 | 
			
		||||
</div>
 | 
			
		||||
<div align="right"><sup>
 | 
			
		||||
@@ -44,231 +41,130 @@ And more...
 | 
			
		||||
  </a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
# Table of Contents
 | 
			
		||||
 | 
			
		||||
- [matterbridge](#matterbridge)
 | 
			
		||||
- [Table of Contents](#table-of-contents)
 | 
			
		||||
  - [Features](#features)
 | 
			
		||||
    - [Natively supported](#natively-supported)
 | 
			
		||||
    - [3rd party via matterbridge api](#3rd-party-via-matterbridge-api)
 | 
			
		||||
    - [API](#api)
 | 
			
		||||
  - [Chat with us](#chat-with-us)
 | 
			
		||||
  - [Screenshots](#screenshots)
 | 
			
		||||
  - [Installing / upgrading](#installing--upgrading)
 | 
			
		||||
    - [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)
 | 
			
		||||
### Table of Contents
 | 
			
		||||
 * [Features](https://github.com/42wim/matterbridge/wiki/Features)
 | 
			
		||||
   * [Natively supported](#natively-supported)
 | 
			
		||||
   * [3rd party via matterbridge api](#3rd-party-via-matterbridge-api)
 | 
			
		||||
   * [API](#API)
 | 
			
		||||
 * [Chat with us](#chat-with-us)
 | 
			
		||||
 * [Screenshots](https://github.com/42wim/matterbridge/wiki/)
 | 
			
		||||
 * [Installing](#installing)
 | 
			
		||||
   * [Binaries](#binaries)
 | 
			
		||||
   * [Building](#building)
 | 
			
		||||
 * [Configuration](#configuration)
 | 
			
		||||
   * [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config)
 | 
			
		||||
   * [Examples](#examples)
 | 
			
		||||
 * [Running](#running)
 | 
			
		||||
   * [Docker](#docker)
 | 
			
		||||
 * [Changelog](#changelog)
 | 
			
		||||
 * [FAQ](#faq)
 | 
			
		||||
 * [Related projects](#related-projects)
 | 
			
		||||
 * [Articles](#articles)
 | 
			
		||||
 * [Thanks](#thanks)
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols)
 | 
			
		||||
- [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols)
 | 
			
		||||
- [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes)
 | 
			
		||||
- Preserves threading when possible
 | 
			
		||||
- [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling)
 | 
			
		||||
- [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing)
 | 
			
		||||
- [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)
 | 
			
		||||
- [API](https://github.com/42wim/matterbridge/wiki/Features#api)
 | 
			
		||||
* [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols)
 | 
			
		||||
* [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols)
 | 
			
		||||
* [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes)
 | 
			
		||||
* Preserves threading when possible
 | 
			
		||||
* [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling)
 | 
			
		||||
* [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing)
 | 
			
		||||
* [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)
 | 
			
		||||
* [API](https://github.com/42wim/matterbridge/wiki/Features#api)
 | 
			
		||||
 | 
			
		||||
### Natively supported
 | 
			
		||||
 | 
			
		||||
- [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)
 | 
			
		||||
- [Mattermost](https://github.com/mattermost/mattermost-server/)
 | 
			
		||||
- [Microsoft Teams](https://teams.microsoft.com)
 | 
			
		||||
- [Mumble](https://www.mumble.info/)
 | 
			
		||||
- [Nextcloud Talk](https://nextcloud.com/talk/)
 | 
			
		||||
- [Rocket.chat](https://rocket.chat)
 | 
			
		||||
- [Slack](https://slack.com)
 | 
			
		||||
- [Ssh-chat](https://github.com/shazow/ssh-chat)
 | 
			
		||||
- ~~[Steam](https://store.steampowered.com/)~~
 | 
			
		||||
  - Not supported anymore, see [here](https://github.com/Philipp15b/go-steam/issues/94) for more info.
 | 
			
		||||
- [Telegram](https://telegram.org)
 | 
			
		||||
- [Twitch](https://twitch.tv)
 | 
			
		||||
- [VK](https://vk.com/)
 | 
			
		||||
- [WhatsApp](https://www.whatsapp.com/)
 | 
			
		||||
  - Whatsapp legacy is natively supported
 | 
			
		||||
  - Whatsapp multidevice beta is natively supported but you need to build yourself, see [here](#building-with-whatsapp-beta-multidevice-support)
 | 
			
		||||
- [XMPP](https://xmpp.org)
 | 
			
		||||
- [Zulip](https://zulipchat.com)
 | 
			
		||||
* [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x
 | 
			
		||||
* [IRC](http://www.mirc.com/servers.html)
 | 
			
		||||
* [XMPP](https://xmpp.org)
 | 
			
		||||
* [Gitter](https://gitter.im)
 | 
			
		||||
* [Slack](https://slack.com)
 | 
			
		||||
* [Discord](https://discordapp.com)
 | 
			
		||||
* [Telegram](https://telegram.org)
 | 
			
		||||
* [Hipchat](https://www.hipchat.com)
 | 
			
		||||
* [Rocket.chat](https://rocket.chat)
 | 
			
		||||
* [Matrix](https://matrix.org)
 | 
			
		||||
* [Steam](https://store.steampowered.com/)
 | 
			
		||||
* [Twitch](https://twitch.tv)
 | 
			
		||||
* [Ssh-chat](https://github.com/shazow/ssh-chat)
 | 
			
		||||
* [WhatsApp](https://www.whatsapp.com/)
 | 
			
		||||
* [Zulip](https://zulipchat.com)
 | 
			
		||||
 | 
			
		||||
### 3rd party via matterbridge api
 | 
			
		||||
 | 
			
		||||
- [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)
 | 
			
		||||
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
 | 
			
		||||
- [Ultima Online Emulator](https://github.com/kuoushi/ServUO-Matterbridge)
 | 
			
		||||
- [Teamspeak](https://github.com/Archeb/ts-matterbridge)
 | 
			
		||||
* [Minecraft](https://github.com/elytra/MatterLink)
 | 
			
		||||
* [Reddit](https://github.com/bonehurtingjuice/mattereddit)
 | 
			
		||||
* [Facebook messenger](https://github.com/VictorNine/fbridge)
 | 
			
		||||
* [Discourse](https://github.com/DeclanHoare/matterbabble)
 | 
			
		||||
 | 
			
		||||
### API
 | 
			
		||||
 | 
			
		||||
The API is basic at the moment.
 | 
			
		||||
The API is very basic at the moment.   
 | 
			
		||||
More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api).
 | 
			
		||||
 | 
			
		||||
Used by the projects below. Feel free to make a PR to add your project to this list.
 | 
			
		||||
 | 
			
		||||
- [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Forge server chat, archived)
 | 
			
		||||
- [MatterCraft](https://github.com/raws/mattercraft) (Matterbridge link for Minecraft Forge server chat)
 | 
			
		||||
- [MatterBukkit](https://gitlab.com/Programie/MatterBukkit) (Matterbridge link for Minecraft Bukkit/Spigot server chat)
 | 
			
		||||
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
 | 
			
		||||
- [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
 | 
			
		||||
- [fbridge-asyncio](https://github.com/powerjungle/fbridge-asyncio) (Facebook messenger support)
 | 
			
		||||
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
 | 
			
		||||
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
 | 
			
		||||
- [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod)
 | 
			
		||||
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
 | 
			
		||||
- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
 | 
			
		||||
- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
 | 
			
		||||
- [beerchat](https://github.com/mt-mods/beerchat) (Matterbridge link for minetest)
 | 
			
		||||
* [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
 | 
			
		||||
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
 | 
			
		||||
* [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
 | 
			
		||||
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
 | 
			
		||||
* [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
 | 
			
		||||
 | 
			
		||||
## Chat with us
 | 
			
		||||
 | 
			
		||||
Questions or want to test on your favorite platform? Join below:
 | 
			
		||||
 | 
			
		||||
- [Discord][mb-discord]
 | 
			
		||||
- [Gitter][mb-gitter]
 | 
			
		||||
- [IRC][mb-irc]
 | 
			
		||||
- [Keybase][mb-keybase]
 | 
			
		||||
- [Matrix][mb-matrix]
 | 
			
		||||
- [Mattermost][mb-mattermost]
 | 
			
		||||
- [Rocket.Chat][mb-rocketchat]
 | 
			
		||||
- [Slack][mb-slack]
 | 
			
		||||
- [Telegram][mb-telegram]
 | 
			
		||||
- [Twitch][mb-twitch]
 | 
			
		||||
- [XMPP][mb-xmpp] (matterbridge@conference.jabber.de)
 | 
			
		||||
- [Zulip][mb-zulip]
 | 
			
		||||
* [Gitter][mb-gitter]
 | 
			
		||||
* [IRC][mb-irc]
 | 
			
		||||
* [Discord][mb-discord]
 | 
			
		||||
* [Matrix][mb-matrix]
 | 
			
		||||
* [Slack][mb-slack]
 | 
			
		||||
* [Mattermost][mb-mattermost]
 | 
			
		||||
* [Rocket.Chat][mb-rocketchat]
 | 
			
		||||
* [XMPP][mb-xmpp]
 | 
			
		||||
* [Twitch][mb-twitch]
 | 
			
		||||
* [Zulip][mb-zulip]
 | 
			
		||||
* [Telegram][mb-telegram]
 | 
			
		||||
 | 
			
		||||
## Screenshots
 | 
			
		||||
See https://github.com/42wim/matterbridge/wiki
 | 
			
		||||
 | 
			
		||||
See <https://github.com/42wim/matterbridge/wiki>
 | 
			
		||||
 | 
			
		||||
## Installing / upgrading
 | 
			
		||||
 | 
			
		||||
## Installing
 | 
			
		||||
### Binaries
 | 
			
		||||
 | 
			
		||||
- 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.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.
 | 
			
		||||
* Latest stable release [v1.14.2](https://github.com/42wim/matterbridge/releases/latest)
 | 
			
		||||
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
 | 
			
		||||
 | 
			
		||||
### Packages
 | 
			
		||||
* [Overview](https://repology.org/metapackage/matterbridge/versions)
 | 
			
		||||
 | 
			
		||||
- [Overview](https://repology.org/metapackage/matterbridge/versions)
 | 
			
		||||
- [snap](https://snapcraft.io/matterbridge)
 | 
			
		||||
- [scoop](https://github.com/42wim/scoop-bucket)
 | 
			
		||||
### Building
 | 
			
		||||
Go 1.9+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH](https://golang.org/doc/code.html#GOPATH).
 | 
			
		||||
 | 
			
		||||
## Building
 | 
			
		||||
After Go is setup, download matterbridge to your $GOPATH directory.
 | 
			
		||||
 | 
			
		||||
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest)
 | 
			
		||||
 | 
			
		||||
If you really want to build from source, follow these instructions:
 | 
			
		||||
Go 1.18+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed.
 | 
			
		||||
 | 
			
		||||
Building the binary with **all** the bridges enabled needs about 3GB RAM to compile.
 | 
			
		||||
You can reduce this memory requirement to 0,5GB RAM by adding the `nomsteams` tag if you don't need/use the Microsoft Teams bridge.
 | 
			
		||||
 | 
			
		||||
Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`)
 | 
			
		||||
 | 
			
		||||
To install the latest stable run:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
go install github.com/42wim/matterbridge
 | 
			
		||||
```
 | 
			
		||||
cd $GOPATH
 | 
			
		||||
go get github.com/42wim/matterbridge
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
To install the latest dev run:
 | 
			
		||||
You should now have matterbridge binary in the bin directory:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
go install github.com/42wim/matterbridge@master
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
To install the latest stable run without msteams or zulip bridge:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
go install -tags nomsteams,nozulip github.com/42wim/matterbridge
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You should now have matterbridge binary in the ~/go/bin directory:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
$ ls ~/go/bin/
 | 
			
		||||
matterbridge
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Building with whatsapp (beta) multidevice support
 | 
			
		||||
 | 
			
		||||
Because the library we use for Whatsapp multidevice support includes a GPL3 library we can not provide you binaries.
 | 
			
		||||
(as this would require the Matterbridge to change it license to GPL)
 | 
			
		||||
 | 
			
		||||
Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`)
 | 
			
		||||
 | 
			
		||||
So this means you have to build it yourself using the instructions below:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
go install -tags whatsappmulti github.com/42wim/matterbridge@master
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If you're low on memory and don't need msteams:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
go install -tags nomsteams,whatsappmulti github.com/42wim/matterbridge@master
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You should now have matterbridge binary in the ~/go/bin directory:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
$ ls ~/go/bin/
 | 
			
		||||
$ ls bin/
 | 
			
		||||
matterbridge
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Configuration
 | 
			
		||||
 | 
			
		||||
### Basic configuration
 | 
			
		||||
 | 
			
		||||
See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
 | 
			
		||||
 | 
			
		||||
### Settings
 | 
			
		||||
 | 
			
		||||
All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for each bridge.
 | 
			
		||||
 | 
			
		||||
### Advanced configuration
 | 
			
		||||
 | 
			
		||||
- [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
 | 
			
		||||
* [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example.
 | 
			
		||||
 | 
			
		||||
### Examples
 | 
			
		||||
 | 
			
		||||
#### Bridge mattermost (off-topic) - irc (#testing)
 | 
			
		||||
 | 
			
		||||
```toml
 | 
			
		||||
[irc]
 | 
			
		||||
    [irc.libera]
 | 
			
		||||
    Server="irc.libera.chat:6667"
 | 
			
		||||
    [irc.freenode]
 | 
			
		||||
    Server="irc.freenode.net:6667"
 | 
			
		||||
    Nick="yourbotname"
 | 
			
		||||
 | 
			
		||||
[mattermost]
 | 
			
		||||
@@ -284,7 +180,7 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for
 | 
			
		||||
name="mygateway"
 | 
			
		||||
enable=true
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
    account="irc.libera"
 | 
			
		||||
    account="irc.freenode"
 | 
			
		||||
    channel="#testing"
 | 
			
		||||
 | 
			
		||||
    [[gateway.inout]]
 | 
			
		||||
@@ -293,7 +189,6 @@ enable=true
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Bridge slack (#general) - discord (general)
 | 
			
		||||
 | 
			
		||||
```toml
 | 
			
		||||
[slack]
 | 
			
		||||
[slack.test]
 | 
			
		||||
@@ -325,7 +220,7 @@ RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
 | 
			
		||||
 | 
			
		||||
See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
```
 | 
			
		||||
Usage of ./matterbridge:
 | 
			
		||||
  -conf string
 | 
			
		||||
        config file (default "matterbridge.toml")
 | 
			
		||||
@@ -338,15 +233,12 @@ Usage of ./matterbridge:
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Docker
 | 
			
		||||
 | 
			
		||||
Please take a look at the [Docker Wiki page](https://github.com/42wim/matterbridge/wiki/Deploy:-Docker) for more information.
 | 
			
		||||
 | 
			
		||||
### Systemd
 | 
			
		||||
 | 
			
		||||
Please take a look at the [Service Files page](https://github.com/42wim/matterbridge/wiki/Service-files) for more information.
 | 
			
		||||
Create your matterbridge.toml file locally eg in `/tmp/matterbridge.toml`
 | 
			
		||||
```
 | 
			
		||||
docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Changelog
 | 
			
		||||
 | 
			
		||||
See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md)
 | 
			
		||||
 | 
			
		||||
## FAQ
 | 
			
		||||
@@ -354,38 +246,28 @@ See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.m
 | 
			
		||||
See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
 | 
			
		||||
 | 
			
		||||
## Related projects
 | 
			
		||||
* [FOSSRIT/infrastructure - roles/matterbridge](https://github.com/FOSSRIT/infrastructure/tree/master/roles/matterbridge) (Ansible role used to automate deployments of Matterbridge)
 | 
			
		||||
* [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig)
 | 
			
		||||
* [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
 | 
			
		||||
* [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
 | 
			
		||||
* [mattereddit](https://github.com/bonehurtingjuice/mattereddit)
 | 
			
		||||
* [matterlink](https://github.com/elytra/MatterLink)
 | 
			
		||||
* [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost
 | 
			
		||||
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
 | 
			
		||||
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
 | 
			
		||||
* [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
 | 
			
		||||
* [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
 | 
			
		||||
 | 
			
		||||
- [jwflory/ansible-role-matterbridge](https://galaxy.ansible.com/jwflory/matterbridge) (Ansible role to simplify deploying Matterbridge)
 | 
			
		||||
- [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig)
 | 
			
		||||
- [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
 | 
			
		||||
- [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
 | 
			
		||||
- [mattereddit](https://github.com/bonehurtingjuice/mattereddit)
 | 
			
		||||
- [matterlink](https://github.com/elytra/MatterLink)
 | 
			
		||||
- [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost
 | 
			
		||||
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
 | 
			
		||||
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
 | 
			
		||||
- [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
 | 
			
		||||
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
 | 
			
		||||
- [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
 | 
			
		||||
 | 
			
		||||
- [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
 | 
			
		||||
- <https://mattermost.com/blog/connect-irc-to-mattermost/>
 | 
			
		||||
- <https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/>
 | 
			
		||||
- <https://blog.brightscout.com/top-10-mattermost-integrations/>
 | 
			
		||||
- <https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/>
 | 
			
		||||
- <https://kopano.com/blog/matterbridge-bridging-mattermost-chat/>
 | 
			
		||||
- <https://www.stitcher.com/s/?eid=52382713>
 | 
			
		||||
- <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/>
 | 
			
		||||
- <https://userlinux.net/mattermost-and-matterbridge.html>
 | 
			
		||||
- <https://nextcloud.com/blog/bridging-chat-services-in-talk/>
 | 
			
		||||
- <https://minecraftchest1.wordpress.com/2021/06/05/how-to-install-and-setup-matterbridge/>
 | 
			
		||||
- Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc)
 | 
			
		||||
## Articles
 | 
			
		||||
* [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
 | 
			
		||||
* https://mattermost.com/blog/connect-irc-to-mattermost/
 | 
			
		||||
* https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/
 | 
			
		||||
* https://blog.brightscout.com/top-10-mattermost-integrations/
 | 
			
		||||
* http://bencey.co.nz/2018/09/17/bridge/
 | 
			
		||||
* https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/
 | 
			
		||||
* https://kopano.com/blog/matterbridge-bridging-mattermost-chat/
 | 
			
		||||
* https://www.stitcher.com/s/?eid=52382713
 | 
			
		||||
* https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/
 | 
			
		||||
 | 
			
		||||
## Thanks
 | 
			
		||||
 | 
			
		||||
@@ -397,46 +279,34 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
- mattermost - <https://github.com/mattermost/mattermost-server>
 | 
			
		||||
- msgraph.go - <https://github.com/yaegashi/msgraph.go>
 | 
			
		||||
- mumble - <https://github.com/layeh/gumble>
 | 
			
		||||
- nctalk - <https://github.com/gary-kim/go-nc-talk>
 | 
			
		||||
- rocketchat - <https://github.com/RocketChat/Rocket.Chat.Go.SDK>
 | 
			
		||||
- slack - <https://github.com/nlopes/slack>
 | 
			
		||||
- sshchat - <https://github.com/shazow/ssh-chat>
 | 
			
		||||
- steam - <https://github.com/Philipp15b/go-steam>
 | 
			
		||||
- telegram - <https://github.com/go-telegram-bot-api/telegram-bot-api>
 | 
			
		||||
- tengo - <https://github.com/d5/tengo>
 | 
			
		||||
- vk - <https://github.com/SevereCloud/vksdk>
 | 
			
		||||
- whatsapp - <https://github.com/Rhymen/go-whatsapp>
 | 
			
		||||
- whatsapp - <https://github.com/tulir/whatsmeow>
 | 
			
		||||
- xmpp - <https://github.com/mattn/go-xmpp>
 | 
			
		||||
- zulip - <https://github.com/ifo/gozulipbot>
 | 
			
		||||
* discord - https://github.com/bwmarrin/discordgo
 | 
			
		||||
* echo - https://github.com/labstack/echo
 | 
			
		||||
* gitter - https://github.com/sromku/go-gitter
 | 
			
		||||
* gops - https://github.com/google/gops
 | 
			
		||||
* gozulipbot - https://github.com/ifo/gozulipbot
 | 
			
		||||
* irc - https://github.com/lrstanley/girc
 | 
			
		||||
* mattermost - https://github.com/mattermost/mattermost-server
 | 
			
		||||
* matrix - https://github.com/matrix-org/gomatrix
 | 
			
		||||
* sshchat - https://github.com/shazow/ssh-chat
 | 
			
		||||
* slack - https://github.com/nlopes/slack
 | 
			
		||||
* steam - https://github.com/Philipp15b/go-steam
 | 
			
		||||
* telegram - https://github.com/go-telegram-bot-api/telegram-bot-api
 | 
			
		||||
* xmpp - https://github.com/mattn/go-xmpp
 | 
			
		||||
* whatsapp - https://github.com/Rhymen/go-whatsapp/
 | 
			
		||||
* zulip - https://github.com/ifo/gozulipbot
 | 
			
		||||
* tengo - https://github.com/d5/tengo
 | 
			
		||||
 | 
			
		||||
<!-- Links -->
 | 
			
		||||
 | 
			
		||||
[mb-discord]: https://discord.gg/AkKPtrQ
 | 
			
		||||
[mb-gitter]: https://gitter.im/42wim/matterbridge
 | 
			
		||||
[mb-irc]: https://web.libera.chat/#matterbridge
 | 
			
		||||
[mb-keybase]: https://keybase.io/team/matterbridge
 | 
			
		||||
[mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org
 | 
			
		||||
[mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e
 | 
			
		||||
[mb-msteams]: https://teams.microsoft.com/join/hj92x75gd3y7
 | 
			
		||||
[mb-rocketchat]: https://open.rocket.chat/channel/matterbridge
 | 
			
		||||
[mb-slack]: https://join.slack.com/t/matterbridgechat/shared_invite/zt-2ourq2h2-7YvyYBq2WFGC~~zEzA68_Q
 | 
			
		||||
[mb-telegram]: https://t.me/Matterbridge
 | 
			
		||||
[mb-twitch]: https://www.twitch.tv/matterbridge
 | 
			
		||||
[mb-whatsapp]: https://www.whatsapp.com/
 | 
			
		||||
[mb-xmpp]: https://inverse.chat/
 | 
			
		||||
[mb-zulip]: https://matterbridge.zulipchat.com/register/
 | 
			
		||||
   [mb-gitter]: https://gitter.im/42wim/matterbridge
 | 
			
		||||
   [mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat
 | 
			
		||||
   [mb-discord]: https://discord.gg/AkKPtrQ
 | 
			
		||||
   [mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org
 | 
			
		||||
   [mb-slack]: https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA
 | 
			
		||||
   [mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e
 | 
			
		||||
   [mb-rocketchat]: https://open.rocket.chat/channel/matterbridge
 | 
			
		||||
   [mb-xmpp]: https://inverse.chat/
 | 
			
		||||
   [mb-twitch]: https://www.twitch.tv/matterbridge
 | 
			
		||||
   [mb-whatsapp]: https://www.whatsapp.com/
 | 
			
		||||
   [mb-zulip]: https://matterbridge.zulipchat.com/register/
 | 
			
		||||
   [mb-telegram]: https://t.me/Matterbridge
 | 
			
		||||
 
 | 
			
		||||
@@ -6,20 +6,17 @@ import (
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"gopkg.in/olahol/melody.v1"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"github.com/labstack/echo/v4/middleware"
 | 
			
		||||
	ring "github.com/zfjagann/golang-ring"
 | 
			
		||||
	"github.com/zfjagann/golang-ring"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type API struct {
 | 
			
		||||
	Messages ring.Ring
 | 
			
		||||
	sync.RWMutex
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
	mrouter *melody.Melody
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Message struct {
 | 
			
		||||
@@ -35,32 +32,6 @@ func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	e := echo.New()
 | 
			
		||||
	e.HideBanner = true
 | 
			
		||||
	e.HidePort = true
 | 
			
		||||
 | 
			
		||||
	b.mrouter = melody.New()
 | 
			
		||||
	b.mrouter.HandleMessage(func(s *melody.Session, msg []byte) {
 | 
			
		||||
		message := config.Message{}
 | 
			
		||||
		err := json.Unmarshal(msg, &message)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("failed to decode message from byte[] '%s'", string(msg))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		b.handleWebsocketMessage(message, s)
 | 
			
		||||
	})
 | 
			
		||||
	b.mrouter.HandleConnect(func(session *melody.Session) {
 | 
			
		||||
		greet := b.getGreeting()
 | 
			
		||||
		data, err := json.Marshal(greet)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("failed to encode message '%v'", greet)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		err = session.Write(data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("failed to write message '%s'", string(data))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// TODO: send message history buffer from `b.Messages` here
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	b.Messages = ring.Ring{}
 | 
			
		||||
	if b.GetInt("Buffer") != 0 {
 | 
			
		||||
		b.Messages.SetCapacity(b.GetInt("Buffer"))
 | 
			
		||||
@@ -70,17 +41,9 @@ func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
			return key == b.GetString("Token"), nil
 | 
			
		||||
		}))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set RemoteNickFormat to a sane default
 | 
			
		||||
	if !b.IsKeySet("RemoteNickFormat") {
 | 
			
		||||
		b.Log.Debugln("RemoteNickFormat is unset, defaulting to \"{NICK}\"")
 | 
			
		||||
		b.Config.Config.Viper().Set(b.GetConfigKey("RemoteNickFormat"), "{NICK}")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	e.GET("/api/health", b.handleHealthcheck)
 | 
			
		||||
	e.GET("/api/messages", b.handleMessages)
 | 
			
		||||
	e.GET("/api/stream", b.handleStream)
 | 
			
		||||
	e.GET("/api/websocket", b.handleWebsocket)
 | 
			
		||||
	e.POST("/api/message", b.handlePostMessage)
 | 
			
		||||
	go func() {
 | 
			
		||||
		if b.GetString("BindAddress") == "" {
 | 
			
		||||
@@ -95,13 +58,13 @@ func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
func (b *API) Connect() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *API) Disconnect() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
func (b *API) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	return nil
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *API) Send(msg config.Message) (string, error) {
 | 
			
		||||
@@ -111,14 +74,7 @@ func (b *API) Send(msg config.Message) (string, error) {
 | 
			
		||||
	if msg.Event == config.EventMsgDelete {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("enqueueing message from %s on ring buffer", msg.Username)
 | 
			
		||||
	b.Messages.Enqueue(msg)
 | 
			
		||||
 | 
			
		||||
	data, err := json.Marshal(msg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("failed to encode message  '%s'", msg)
 | 
			
		||||
	}
 | 
			
		||||
	_ = b.mrouter.Broadcast(data)
 | 
			
		||||
	b.Messages.Enqueue(&msg)
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -150,23 +106,18 @@ func (b *API) handleMessages(c echo.Context) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *API) getGreeting() config.Message {
 | 
			
		||||
	return config.Message{
 | 
			
		||||
		Event:     config.EventAPIConnected,
 | 
			
		||||
		Timestamp: time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *API) handleStream(c echo.Context) error {
 | 
			
		||||
	c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
 | 
			
		||||
	c.Response().WriteHeader(http.StatusOK)
 | 
			
		||||
	greet := b.getGreeting()
 | 
			
		||||
	greet := config.Message{
 | 
			
		||||
		Event:     config.EventAPIConnected,
 | 
			
		||||
		Timestamp: time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
	if err := json.NewEncoder(c.Response()).Encode(greet); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	c.Response().Flush()
 | 
			
		||||
	for {
 | 
			
		||||
		// 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 {
 | 
			
		||||
@@ -177,31 +128,3 @@ func (b *API) handleStream(c echo.Context) error {
 | 
			
		||||
		time.Sleep(200 * time.Millisecond)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *API) handleWebsocketMessage(message config.Message, s *melody.Session) {
 | 
			
		||||
	message.Channel = "api"
 | 
			
		||||
	message.Protocol = "api"
 | 
			
		||||
	message.Account = b.Account
 | 
			
		||||
	message.ID = ""
 | 
			
		||||
	message.Timestamp = time.Now()
 | 
			
		||||
 | 
			
		||||
	data, err := json.Marshal(message)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("failed to encode message for loopback '%v'", message)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	_ = b.mrouter.BroadcastOthers(data, s)
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Sending websocket message from %s on %s to gateway", message.Username, "api")
 | 
			
		||||
	b.Remote <- message
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *API) handleWebsocket(c echo.Context) error {
 | 
			
		||||
	err := b.mrouter.HandleRequest(c.Response(), c.Request())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("error in websocket handling  '%v'", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,8 @@
 | 
			
		||||
package bridge
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
@@ -43,10 +41,6 @@ type Factory func(*Config) Bridger
 | 
			
		||||
 | 
			
		||||
func New(bridge *config.Bridge) *Bridge {
 | 
			
		||||
	accInfo := strings.Split(bridge.Account, ".")
 | 
			
		||||
	if len(accInfo) != 2 {
 | 
			
		||||
		log.Fatalf("config failure, account incorrect: %s", bridge.Account)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	protocol := accInfo[0]
 | 
			
		||||
	name := accInfo[1]
 | 
			
		||||
 | 
			
		||||
@@ -75,7 +69,6 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map
 | 
			
		||||
	for ID, channel := range channels {
 | 
			
		||||
		if !exists[ID] {
 | 
			
		||||
			b.Log.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID)
 | 
			
		||||
			time.Sleep(time.Duration(b.GetInt("JoinDelay")) * time.Millisecond)
 | 
			
		||||
			err := b.JoinChannel(channel)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
@@ -86,16 +79,8 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bridge) GetConfigKey(key string) string {
 | 
			
		||||
	return b.Account + "." + key
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bridge) IsKeySet(key string) bool {
 | 
			
		||||
	return b.Config.IsKeySet(b.GetConfigKey(key)) || b.Config.IsKeySet("general."+key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bridge) GetBool(key string) bool {
 | 
			
		||||
	val, ok := b.Config.GetBool(b.GetConfigKey(key))
 | 
			
		||||
	val, ok := b.Config.GetBool(b.Account + "." + key)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		val, _ = b.Config.GetBool("general." + key)
 | 
			
		||||
	}
 | 
			
		||||
@@ -103,7 +88,7 @@ func (b *Bridge) GetBool(key string) bool {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bridge) GetInt(key string) int {
 | 
			
		||||
	val, ok := b.Config.GetInt(b.GetConfigKey(key))
 | 
			
		||||
	val, ok := b.Config.GetInt(b.Account + "." + key)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		val, _ = b.Config.GetInt("general." + key)
 | 
			
		||||
	}
 | 
			
		||||
@@ -111,7 +96,7 @@ func (b *Bridge) GetInt(key string) int {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bridge) GetString(key string) string {
 | 
			
		||||
	val, ok := b.Config.GetString(b.GetConfigKey(key))
 | 
			
		||||
	val, ok := b.Config.GetString(b.Account + "." + key)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		val, _ = b.Config.GetString("general." + key)
 | 
			
		||||
	}
 | 
			
		||||
@@ -119,7 +104,7 @@ func (b *Bridge) GetString(key string) string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bridge) GetStringSlice(key string) []string {
 | 
			
		||||
	val, ok := b.Config.GetStringSlice(b.GetConfigKey(key))
 | 
			
		||||
	val, ok := b.Config.GetStringSlice(b.Account + "." + key)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		val, _ = b.Config.GetStringSlice("general." + key)
 | 
			
		||||
	}
 | 
			
		||||
@@ -127,7 +112,7 @@ func (b *Bridge) GetStringSlice(key string) []string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bridge) GetStringSlice2D(key string) [][]string {
 | 
			
		||||
	val, ok := b.Config.GetStringSlice2D(b.GetConfigKey(key))
 | 
			
		||||
	val, ok := b.Config.GetStringSlice2D(b.Account + "." + key)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		val, _ = b.Config.GetStringSlice2D("general." + key)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,6 @@ package config
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -23,15 +21,11 @@ const (
 | 
			
		||||
	EventRejoinChannels    = "rejoin_channels"
 | 
			
		||||
	EventUserAction        = "user_action"
 | 
			
		||||
	EventMsgDelete         = "msg_delete"
 | 
			
		||||
	EventFileDelete        = "file_delete"
 | 
			
		||||
	EventAPIConnected      = "api_connected"
 | 
			
		||||
	EventUserTyping        = "user_typing"
 | 
			
		||||
	EventGetChannelMembers = "get_channel_members"
 | 
			
		||||
	EventNoticeIRC         = "notice_irc"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const ParentIDNotFound = "msg-parent-not-found"
 | 
			
		||||
 | 
			
		||||
type Message struct {
 | 
			
		||||
	Text      string    `json:"text"`
 | 
			
		||||
	Channel   string    `json:"channel"`
 | 
			
		||||
@@ -48,23 +42,14 @@ type Message struct {
 | 
			
		||||
	Extra     map[string][]interface{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m Message) ParentNotFound() bool {
 | 
			
		||||
	return m.ParentID == ParentIDNotFound
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m Message) ParentValid() bool {
 | 
			
		||||
	return m.ParentID != "" && !m.ParentNotFound()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FileInfo struct {
 | 
			
		||||
	Name     string
 | 
			
		||||
	Data     *[]byte
 | 
			
		||||
	Comment  string
 | 
			
		||||
	URL      string
 | 
			
		||||
	Size     int64
 | 
			
		||||
	Avatar   bool
 | 
			
		||||
	SHA      string
 | 
			
		||||
	NativeID string
 | 
			
		||||
	Name    string
 | 
			
		||||
	Data    *[]byte
 | 
			
		||||
	Comment string
 | 
			
		||||
	URL     string
 | 
			
		||||
	Size    int64
 | 
			
		||||
	Avatar  bool
 | 
			
		||||
	SHA     string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChannelInfo struct {
 | 
			
		||||
@@ -87,34 +72,27 @@ type ChannelMember struct {
 | 
			
		||||
type ChannelMembers []ChannelMember
 | 
			
		||||
 | 
			
		||||
type Protocol struct {
 | 
			
		||||
	AllowMention           []string // discord
 | 
			
		||||
	AuthCode               string   // steam
 | 
			
		||||
	BindAddress            string   // mattermost, slack // DEPRECATED
 | 
			
		||||
	Buffer                 int      // api
 | 
			
		||||
	Charset                string   // irc
 | 
			
		||||
	ClientID               string   // msteams
 | 
			
		||||
	ColorNicks             bool     // only irc for now
 | 
			
		||||
	Debug                  bool     // general
 | 
			
		||||
	DebugLevel             int      // only for irc now
 | 
			
		||||
	DisableWebPagePreview  bool     // telegram
 | 
			
		||||
	EditSuffix             string   // mattermost, slack, discord, telegram, gitter
 | 
			
		||||
	EditDisable            bool     // mattermost, slack, discord, telegram, gitter
 | 
			
		||||
	HTMLDisable            bool     // matrix
 | 
			
		||||
	IconURL                string   // mattermost, slack
 | 
			
		||||
	IgnoreFailureOnStart   bool     // general
 | 
			
		||||
	IgnoreNicks            string   // all protocols
 | 
			
		||||
	IgnoreMessages         string   // all protocols
 | 
			
		||||
	Jid                    string   // xmpp
 | 
			
		||||
	JoinDelay              string   // all protocols
 | 
			
		||||
	Label                  string   // all protocols
 | 
			
		||||
	Login                  string   // mattermost, matrix
 | 
			
		||||
	LogFile                string   // general
 | 
			
		||||
	AuthCode               string // steam
 | 
			
		||||
	BindAddress            string // mattermost, slack // DEPRECATED
 | 
			
		||||
	Buffer                 int    // api
 | 
			
		||||
	Charset                string // irc
 | 
			
		||||
	ColorNicks             bool   // only irc for now
 | 
			
		||||
	Debug                  bool   // general
 | 
			
		||||
	DebugLevel             int    // only for irc now
 | 
			
		||||
	EditSuffix             string // mattermost, slack, discord, telegram, gitter
 | 
			
		||||
	EditDisable            bool   // mattermost, slack, discord, telegram, gitter
 | 
			
		||||
	IconURL                string // mattermost, slack
 | 
			
		||||
	IgnoreFailureOnStart   bool   // general
 | 
			
		||||
	IgnoreNicks            string // all protocols
 | 
			
		||||
	IgnoreMessages         string // all protocols
 | 
			
		||||
	Jid                    string // xmpp
 | 
			
		||||
	Label                  string // all protocols
 | 
			
		||||
	Login                  string // mattermost, matrix
 | 
			
		||||
	MediaDownloadBlackList []string
 | 
			
		||||
	MediaDownloadPath      string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server.
 | 
			
		||||
	MediaDownloadSize      int    // all protocols
 | 
			
		||||
	MediaServerDownload    string
 | 
			
		||||
	MediaServerUpload      string
 | 
			
		||||
	MediaConvertTgs        string     // telegram
 | 
			
		||||
	MediaConvertWebPToPNG  bool       // telegram
 | 
			
		||||
	MessageDelay           int        // IRC, time in millisecond to wait between messages
 | 
			
		||||
	MessageFormat          string     // telegram
 | 
			
		||||
@@ -122,7 +100,6 @@ type Protocol struct {
 | 
			
		||||
	MessageQueue           int        // IRC, size of message queue for flood control
 | 
			
		||||
	MessageSplit           bool       // IRC, split long messages with newlines on MessageLength instead of clipping
 | 
			
		||||
	Muc                    string     // xmpp
 | 
			
		||||
	MxID                   string     // matrix
 | 
			
		||||
	Name                   string     // all protocols
 | 
			
		||||
	Nick                   string     // all protocols
 | 
			
		||||
	NickFormatter          string     // mattermost, slack
 | 
			
		||||
@@ -132,48 +109,38 @@ type Protocol struct {
 | 
			
		||||
	NicksPerRow            int        // mattermost, slack
 | 
			
		||||
	NoHomeServerSuffix     bool       // matrix
 | 
			
		||||
	NoSendJoinPart         bool       // all protocols
 | 
			
		||||
	NoTLS                  bool       // mattermost, xmpp
 | 
			
		||||
	NoTLS                  bool       // mattermost
 | 
			
		||||
	Password               string     // IRC,mattermost,XMPP,matrix
 | 
			
		||||
	PrefixMessagesWithNick bool       // mattemost, slack
 | 
			
		||||
	PreserveThreading      bool       // slack
 | 
			
		||||
	Protocol               string     // all protocols
 | 
			
		||||
	QuoteDisable           bool       // telegram
 | 
			
		||||
	QuoteFormat            string     // telegram
 | 
			
		||||
	QuoteLengthLimit       int        // telegram
 | 
			
		||||
	RealName               string     // IRC
 | 
			
		||||
	RejoinDelay            int        // IRC
 | 
			
		||||
	ReplaceMessages        [][]string // all protocols
 | 
			
		||||
	ReplaceNicks           [][]string // all protocols
 | 
			
		||||
	RemoteNickFormat       string     // all protocols
 | 
			
		||||
	RunCommands            []string   // IRC
 | 
			
		||||
	Server                 string     // IRC,mattermost,XMPP,discord,matrix
 | 
			
		||||
	SessionFile            string     // msteams,whatsapp
 | 
			
		||||
	RunCommands            []string   // irc
 | 
			
		||||
	Server                 string     // IRC,mattermost,XMPP,discord
 | 
			
		||||
	ShowJoinPart           bool       // all protocols
 | 
			
		||||
	ShowTopicChange        bool       // slack
 | 
			
		||||
	ShowUserTyping         bool       // slack
 | 
			
		||||
	ShowEmbeds             bool       // discord
 | 
			
		||||
	SkipTLSVerify          bool       // IRC, mattermost
 | 
			
		||||
	SkipVersionCheck       bool       // mattermost
 | 
			
		||||
	StripNick              bool       // all protocols
 | 
			
		||||
	StripMarkdown          bool       // irc
 | 
			
		||||
	SyncTopic              bool       // slack
 | 
			
		||||
	TengoModifyMessage     string     // general
 | 
			
		||||
	Team                   string     // mattermost, keybase
 | 
			
		||||
	TeamID                 string     // msteams
 | 
			
		||||
	TenantID               string     // msteams
 | 
			
		||||
	Token                  string     // gitter, slack, discord, api, matrix
 | 
			
		||||
	Team                   string     // mattermost
 | 
			
		||||
	Token                  string     // gitter, slack, discord, api
 | 
			
		||||
	Topic                  string     // zulip
 | 
			
		||||
	URL                    string     // mattermost, slack // DEPRECATED
 | 
			
		||||
	UseAPI                 bool       // mattermost, slack
 | 
			
		||||
	UseLocalAvatar         []string   // discord
 | 
			
		||||
	UseSASL                bool       // IRC
 | 
			
		||||
	UseTLS                 bool       // IRC
 | 
			
		||||
	UseDiscriminator       bool       // discord
 | 
			
		||||
	UseFirstName           bool       // telegram
 | 
			
		||||
	UseUserName            bool       // discord, matrix, mattermost
 | 
			
		||||
	UseUserName            bool       // discord
 | 
			
		||||
	UseInsecureURL         bool       // telegram
 | 
			
		||||
	UserName               string     // IRC
 | 
			
		||||
	VerboseJoinPart        bool       // IRC
 | 
			
		||||
	WebhookBindAddress     string     // mattermost, slack
 | 
			
		||||
	WebhookURL             string     // mattermost, slack
 | 
			
		||||
}
 | 
			
		||||
@@ -199,13 +166,6 @@ type Gateway struct {
 | 
			
		||||
	InOut  []Bridge
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Tengo struct {
 | 
			
		||||
	InMessage        string
 | 
			
		||||
	Message          string
 | 
			
		||||
	RemoteNickFormat string
 | 
			
		||||
	OutMessage       string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SameChannelGateway struct {
 | 
			
		||||
	Name     string
 | 
			
		||||
	Enable   bool
 | 
			
		||||
@@ -229,18 +189,13 @@ type BridgeValues struct {
 | 
			
		||||
	SSHChat            map[string]Protocol
 | 
			
		||||
	WhatsApp           map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results
 | 
			
		||||
	Zulip              map[string]Protocol
 | 
			
		||||
	Keybase            map[string]Protocol
 | 
			
		||||
	Mumble             map[string]Protocol
 | 
			
		||||
	General            Protocol
 | 
			
		||||
	Tengo              Tengo
 | 
			
		||||
	Gateway            []Gateway
 | 
			
		||||
	SameChannelGateway []SameChannelGateway
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Config interface {
 | 
			
		||||
	Viper() *viper.Viper
 | 
			
		||||
	BridgeValues() *BridgeValues
 | 
			
		||||
	IsKeySet(key string) bool
 | 
			
		||||
	GetBool(key string) (bool, bool)
 | 
			
		||||
	GetInt(key string) (int, bool)
 | 
			
		||||
	GetString(key string) (string, bool)
 | 
			
		||||
@@ -266,17 +221,7 @@ func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
 | 
			
		||||
		logger.Fatalf("Failed to read configuration file: %#v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfgtype := detectConfigType(cfgfile)
 | 
			
		||||
	mycfg := newConfigFromString(logger, input, cfgtype)
 | 
			
		||||
	if mycfg.cv.General.LogFile != "" {
 | 
			
		||||
		logfile, err := os.OpenFile(mycfg.cv.General.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			logger.Info("Opening log file ", mycfg.cv.General.LogFile)
 | 
			
		||||
			rootLogger.Out = logfile
 | 
			
		||||
		} else {
 | 
			
		||||
			logger.Warn("Failed to open ", mycfg.cv.General.LogFile)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	mycfg := newConfigFromString(logger, input)
 | 
			
		||||
	if mycfg.cv.General.MediaDownloadSize == 0 {
 | 
			
		||||
		mycfg.cv.General.MediaDownloadSize = 1000000
 | 
			
		||||
	}
 | 
			
		||||
@@ -287,37 +232,25 @@ func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
 | 
			
		||||
	return mycfg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// detectConfigType detects JSON and YAML formats, defaults to TOML.
 | 
			
		||||
func detectConfigType(cfgfile string) string {
 | 
			
		||||
	fileExt := filepath.Ext(cfgfile)
 | 
			
		||||
	switch fileExt {
 | 
			
		||||
	case ".json":
 | 
			
		||||
		return "json"
 | 
			
		||||
	case ".yaml", ".yml":
 | 
			
		||||
		return "yaml"
 | 
			
		||||
	}
 | 
			
		||||
	return "toml"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewConfigFromString instantiates a new configuration based on the specified string.
 | 
			
		||||
func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config {
 | 
			
		||||
	logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
 | 
			
		||||
	return newConfigFromString(logger, input, "toml")
 | 
			
		||||
	return newConfigFromString(logger, input)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newConfigFromString(logger *logrus.Entry, input []byte, cfgtype string) *config {
 | 
			
		||||
	viper.SetConfigType(cfgtype)
 | 
			
		||||
func newConfigFromString(logger *logrus.Entry, input []byte) *config {
 | 
			
		||||
	viper.SetConfigType("toml")
 | 
			
		||||
	viper.SetEnvPrefix("matterbridge")
 | 
			
		||||
	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
 | 
			
		||||
	viper.AutomaticEnv()
 | 
			
		||||
 | 
			
		||||
	if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil {
 | 
			
		||||
		logger.Fatalf("Failed to parse the configuration: %s", err)
 | 
			
		||||
		logger.Fatalf("Failed to parse the configuration: %#v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfg := &BridgeValues{}
 | 
			
		||||
	if err := viper.Unmarshal(cfg); err != nil {
 | 
			
		||||
		logger.Fatalf("Failed to load the configuration: %s", err)
 | 
			
		||||
		logger.Fatalf("Failed to load the configuration: %#v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return &config{
 | 
			
		||||
		logger: logger,
 | 
			
		||||
@@ -330,16 +263,6 @@ func (c *config) BridgeValues() *BridgeValues {
 | 
			
		||||
	return c.cv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *config) Viper() *viper.Viper {
 | 
			
		||||
	return c.v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *config) IsKeySet(key string) bool {
 | 
			
		||||
	c.RLock()
 | 
			
		||||
	defer c.RUnlock()
 | 
			
		||||
	return c.v.IsSet(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *config) GetBool(key string) (bool, bool) {
 | 
			
		||||
	c.RLock()
 | 
			
		||||
	defer c.RUnlock()
 | 
			
		||||
@@ -399,11 +322,6 @@ type TestConfig struct {
 | 
			
		||||
	Overrides map[string]interface{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *TestConfig) IsKeySet(key string) bool {
 | 
			
		||||
	_, ok := c.Overrides[key]
 | 
			
		||||
	return ok || c.Config.IsKeySet(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *TestConfig) GetBool(key string) (bool, bool) {
 | 
			
		||||
	val, ok := c.Overrides[key]
 | 
			
		||||
	if ok {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,31 +2,30 @@ package bdiscord
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/discord/transmitter"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
	lru "github.com/hashicorp/golang-lru"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	MessageLength = 1950
 | 
			
		||||
	cFileUpload   = "file_upload"
 | 
			
		||||
)
 | 
			
		||||
const MessageLength = 1950
 | 
			
		||||
 | 
			
		||||
type Bdiscord struct {
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
 | 
			
		||||
	c *discordgo.Session
 | 
			
		||||
 | 
			
		||||
	nick    string
 | 
			
		||||
	userID  string
 | 
			
		||||
	guildID string
 | 
			
		||||
	nick            string
 | 
			
		||||
	useChannelID    bool
 | 
			
		||||
	guildID         string
 | 
			
		||||
	webhookID       string
 | 
			
		||||
	webhookToken    string
 | 
			
		||||
	canEditWebhooks bool
 | 
			
		||||
 | 
			
		||||
	channelsMutex  sync.RWMutex
 | 
			
		||||
	channels       []*discordgo.Channel
 | 
			
		||||
@@ -35,39 +34,30 @@ type Bdiscord struct {
 | 
			
		||||
	membersMutex  sync.RWMutex
 | 
			
		||||
	userMemberMap map[string]*discordgo.Member
 | 
			
		||||
	nickMemberMap map[string]*discordgo.Member
 | 
			
		||||
 | 
			
		||||
	// Webhook specific logic
 | 
			
		||||
	useAutoWebhooks bool
 | 
			
		||||
	transmitter     *transmitter.Transmitter
 | 
			
		||||
	cache           *lru.Cache
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	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 := &Bdiscord{Config: cfg}
 | 
			
		||||
	b.userMemberMap = make(map[string]*discordgo.Member)
 | 
			
		||||
	b.nickMemberMap = make(map[string]*discordgo.Member)
 | 
			
		||||
	b.channelInfoMap = make(map[string]*config.ChannelInfo)
 | 
			
		||||
 | 
			
		||||
	b.useAutoWebhooks = b.GetBool("AutoWebhooks")
 | 
			
		||||
	if b.useAutoWebhooks {
 | 
			
		||||
		b.Log.Debug("Using automatic webhooks")
 | 
			
		||||
	if b.GetString("WebhookURL") != "" {
 | 
			
		||||
		b.Log.Debug("Configuring Discord Incoming Webhook")
 | 
			
		||||
		b.webhookID, b.webhookToken = b.splitURL(b.GetString("WebhookURL"))
 | 
			
		||||
	}
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) Connect() error {
 | 
			
		||||
	var err error
 | 
			
		||||
	var guildFound bool
 | 
			
		||||
	token := b.GetString("Token")
 | 
			
		||||
	b.Log.Info("Connecting")
 | 
			
		||||
	if b.GetString("WebhookURL") == "" {
 | 
			
		||||
		b.Log.Info("Connecting using token")
 | 
			
		||||
	} else {
 | 
			
		||||
		b.Log.Info("Connecting using webhookurl (for posting) and token")
 | 
			
		||||
	}
 | 
			
		||||
	if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
 | 
			
		||||
		token = "Bot " + b.GetString("Token")
 | 
			
		||||
	}
 | 
			
		||||
@@ -82,21 +72,11 @@ func (b *Bdiscord) Connect() error {
 | 
			
		||||
	}
 | 
			
		||||
	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)
 | 
			
		||||
	b.c.AddHandler(b.memberUpdate)
 | 
			
		||||
	if b.GetInt("debuglevel") == 1 {
 | 
			
		||||
		b.c.AddHandler(b.messageEvent)
 | 
			
		||||
	}
 | 
			
		||||
	// Add privileged intent for guild member tracking. This is needed to track nicks
 | 
			
		||||
	// for display names and @mention translation
 | 
			
		||||
	b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
 | 
			
		||||
		discordgo.IntentsGuildMembers)
 | 
			
		||||
 | 
			
		||||
	err = b.c.Open()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -111,108 +91,55 @@ func (b *Bdiscord) Connect() error {
 | 
			
		||||
	}
 | 
			
		||||
	serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
 | 
			
		||||
	b.nick = userinfo.Username
 | 
			
		||||
	b.userID = userinfo.ID
 | 
			
		||||
 | 
			
		||||
	// Try and find this account's guild, and populate channels
 | 
			
		||||
	b.channelsMutex.Lock()
 | 
			
		||||
	for _, guild := range guilds {
 | 
			
		||||
		// Skip, if the server name does not match the visible name or the ID
 | 
			
		||||
		if guild.Name != serverName && guild.ID != serverName {
 | 
			
		||||
			continue
 | 
			
		||||
		if guild.Name == serverName || guild.ID == serverName {
 | 
			
		||||
			b.channels, err = b.c.GuildChannels(guild.ID)
 | 
			
		||||
			b.guildID = guild.ID
 | 
			
		||||
			guildFound = true
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Complain about an ambiguous Server setting. Two Discord servers could have the same title!
 | 
			
		||||
		// For IDs, practically this will never happen. It would only trigger if some server's name is also an ID.
 | 
			
		||||
		if b.guildID != "" {
 | 
			
		||||
			return fmt.Errorf("found multiple Discord servers with the same name %#v, expected to see only one", serverName)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Getting this guild's channel could result in a permission error
 | 
			
		||||
		b.channels, err = b.c.GuildChannels(guild.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("could not get %#v's channels: %w", b.GetString("Server"), err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.guildID = guild.ID
 | 
			
		||||
	}
 | 
			
		||||
	b.channelsMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	// If we couldn't find a guild, we print extra debug information and return a nice error
 | 
			
		||||
	if b.guildID == "" {
 | 
			
		||||
		err = fmt.Errorf("could not find Discord server %#v", b.GetString("Server"))
 | 
			
		||||
		b.Log.Error(err.Error())
 | 
			
		||||
 | 
			
		||||
		// Print all of the possible server values
 | 
			
		||||
		b.Log.Info("Possible server values:")
 | 
			
		||||
	if !guildFound {
 | 
			
		||||
		msg := fmt.Sprintf("Server \"%s\" not found", b.GetString("Server"))
 | 
			
		||||
		err = errors.New(msg)
 | 
			
		||||
		b.Log.Error(msg)
 | 
			
		||||
		b.Log.Info("Possible values:")
 | 
			
		||||
		for _, guild := range guilds {
 | 
			
		||||
			b.Log.Infof("\t- Server=%#v # by name", guild.Name)
 | 
			
		||||
			b.Log.Infof("\t- Server=%#v # by ID", guild.ID)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If there are no results, we should say that
 | 
			
		||||
		if len(guilds) == 0 {
 | 
			
		||||
			b.Log.Info("\t- (none found)")
 | 
			
		||||
			b.Log.Infof("Server=\"%s\" # Server name", guild.Name)
 | 
			
		||||
			b.Log.Infof("Server=\"%s\" # Server ID", guild.ID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Legacy note: WebhookURL used to have an actual webhook URL that we would edit,
 | 
			
		||||
	// but we stopped doing that due to Discord making rate limits more aggressive.
 | 
			
		||||
	//
 | 
			
		||||
	// Even older: the same WebhookURL used to be used by every channel, which is usually unexpected.
 | 
			
		||||
	// This is no longer possible.
 | 
			
		||||
	if b.GetString("WebhookURL") != "" {
 | 
			
		||||
		message := "The global WebhookURL setting has been removed. "
 | 
			
		||||
		message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. "
 | 
			
		||||
		message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections."
 | 
			
		||||
		b.Log.Errorln(message)
 | 
			
		||||
		return fmt.Errorf("use of removed WebhookURL setting")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.GetInt("debuglevel") == 2 {
 | 
			
		||||
		b.Log.Debug("enabling even more discord debug")
 | 
			
		||||
		b.c.Debug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Initialise webhook management
 | 
			
		||||
	b.transmitter = transmitter.New(b.c, b.guildID, "matterbridge", b.useAutoWebhooks)
 | 
			
		||||
	b.transmitter.Log = b.Log
 | 
			
		||||
 | 
			
		||||
	var webhookChannelIDs []string
 | 
			
		||||
	for _, channel := range b.Channels {
 | 
			
		||||
		channelID := b.getChannelID(channel.Name) // note(qaisjp): this readlocks channelsMutex
 | 
			
		||||
 | 
			
		||||
		// If a WebhookURL was not explicitly provided for this channel,
 | 
			
		||||
		// there are two options: just a regular bot message (ugly) or this is should be webhook sent
 | 
			
		||||
		if channel.Options.WebhookURL == "" {
 | 
			
		||||
			// If it should be webhook sent, we should enforce this via the transmitter
 | 
			
		||||
			if b.useAutoWebhooks {
 | 
			
		||||
				webhookChannelIDs = append(webhookChannelIDs, channelID)
 | 
			
		||||
	b.channelsMutex.RLock()
 | 
			
		||||
	if b.GetString("WebhookURL") == "" {
 | 
			
		||||
		for _, channel := range b.channels {
 | 
			
		||||
			b.Log.Debugf("found channel %#v", channel)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		b.canEditWebhooks = true
 | 
			
		||||
		for _, channel := range b.channels {
 | 
			
		||||
			b.Log.Debugf("found channel %#v; verifying PermissionManageWebhooks", channel)
 | 
			
		||||
			perms, permsErr := b.c.State.UserChannelPermissions(userinfo.ID, channel.ID)
 | 
			
		||||
			manageWebhooks := discordgo.PermissionManageWebhooks
 | 
			
		||||
			if permsErr != nil || perms&manageWebhooks != manageWebhooks {
 | 
			
		||||
				b.Log.Warnf("Can't manage webhooks in channel \"%s\"", channel.Name)
 | 
			
		||||
				b.canEditWebhooks = false
 | 
			
		||||
			}
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		whID, whToken, ok := b.splitURL(channel.Options.WebhookURL)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return fmt.Errorf("failed to parse WebhookURL %#v for channel %#v", channel.Options.WebhookURL, channel.ID)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.transmitter.AddWebhook(channelID, &discordgo.Webhook{
 | 
			
		||||
			ID:        whID,
 | 
			
		||||
			Token:     whToken,
 | 
			
		||||
			GuildID:   b.guildID,
 | 
			
		||||
			ChannelID: channelID,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.useAutoWebhooks {
 | 
			
		||||
		err = b.transmitter.RefreshGuildWebhooks(webhookChannelIDs)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.WithError(err).Println("transmitter could not refresh guild webhooks")
 | 
			
		||||
			return err
 | 
			
		||||
		if b.canEditWebhooks {
 | 
			
		||||
			b.Log.Info("Can manage webhooks; will edit channel for global webhook on send")
 | 
			
		||||
		} else {
 | 
			
		||||
			b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	b.channelsMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	// Obtaining guild members and initializing nickname mapping.
 | 
			
		||||
	b.membersMutex.Lock()
 | 
			
		||||
@@ -245,6 +172,10 @@ func (b *Bdiscord) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	defer b.channelsMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	b.channelInfoMap[channel.ID] = &channel
 | 
			
		||||
	idcheck := strings.Split(channel.Name, "ID:")
 | 
			
		||||
	if len(idcheck) > 1 {
 | 
			
		||||
		b.useChannelID = true
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -256,35 +187,81 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
 | 
			
		||||
		return "", fmt.Errorf("Could not find channelID for %v", msg.Channel)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.Event == config.EventUserTyping {
 | 
			
		||||
		if b.GetBool("ShowUserTyping") {
 | 
			
		||||
			err := b.c.ChannelTyping(channelID)
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Make a action /me of the message
 | 
			
		||||
	if msg.Event == config.EventUserAction {
 | 
			
		||||
		msg.Text = "_" + msg.Text + "_"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle prefix hint for unthreaded messages.
 | 
			
		||||
	if msg.ParentNotFound() {
 | 
			
		||||
		msg.ParentID = ""
 | 
			
		||||
	// use initial webhook configured for the entire Discord account
 | 
			
		||||
	isGlobalWebhook := true
 | 
			
		||||
	wID := b.webhookID
 | 
			
		||||
	wToken := b.webhookToken
 | 
			
		||||
 | 
			
		||||
	// check if have a channel specific webhook
 | 
			
		||||
	b.channelsMutex.RLock()
 | 
			
		||||
	if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
 | 
			
		||||
		if ci.Options.WebhookURL != "" {
 | 
			
		||||
			wID, wToken = b.splitURL(ci.Options.WebhookURL)
 | 
			
		||||
			isGlobalWebhook = false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	b.channelsMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	// Use webhook to send the message
 | 
			
		||||
	useWebhooks := b.shouldMessageUseWebhooks(&msg)
 | 
			
		||||
	if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" {
 | 
			
		||||
		return b.handleEventWebhook(&msg, channelID)
 | 
			
		||||
	if wID != "" {
 | 
			
		||||
		// skip events
 | 
			
		||||
		if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
		b.Log.Debugf("Broadcasting using Webhook")
 | 
			
		||||
		for _, f := range msg.Extra["file"] {
 | 
			
		||||
			fi := f.(config.FileInfo)
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				msg.Text += fi.Comment + ": "
 | 
			
		||||
			}
 | 
			
		||||
			if fi.URL != "" {
 | 
			
		||||
				msg.Text = fi.URL
 | 
			
		||||
				if fi.Comment != "" {
 | 
			
		||||
					msg.Text = fi.Comment + ": " + fi.URL
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// skip empty messages
 | 
			
		||||
		if msg.Text == "" {
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		msg.Text = helper.ClipMessage(msg.Text, MessageLength)
 | 
			
		||||
		msg.Text = b.replaceUserMentions(msg.Text)
 | 
			
		||||
		// discord username must be [0..32] max
 | 
			
		||||
		if len(msg.Username) > 32 {
 | 
			
		||||
			msg.Username = msg.Username[0:32]
 | 
			
		||||
		}
 | 
			
		||||
		// if we have a global webhook for this Discord account, and permission
 | 
			
		||||
		// to modify webhooks (previously verified), then set its channel to
 | 
			
		||||
		// the message channel before using it
 | 
			
		||||
		// TODO: this isn't necessary if the last message from this webhook was
 | 
			
		||||
		// sent to the current channel
 | 
			
		||||
		if isGlobalWebhook && b.canEditWebhooks {
 | 
			
		||||
			b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel)
 | 
			
		||||
			_, err := b.c.WebhookEdit(wID, "", "", channelID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Errorf("Could not set webhook channel: %s", err)
 | 
			
		||||
				return "", err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		err := b.c.WebhookExecute(
 | 
			
		||||
			wID,
 | 
			
		||||
			wToken,
 | 
			
		||||
			true,
 | 
			
		||||
			&discordgo.WebhookParams{
 | 
			
		||||
				Content:   msg.Text,
 | 
			
		||||
				Username:  msg.Username,
 | 
			
		||||
				AvatarURL: msg.Avatar,
 | 
			
		||||
			})
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b.handleEventBotUser(&msg, channelID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleEventDirect handles events via the bot user
 | 
			
		||||
func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) {
 | 
			
		||||
	b.Log.Debugf("Broadcasting using token (API)")
 | 
			
		||||
 | 
			
		||||
	// Delete message
 | 
			
		||||
@@ -296,36 +273,21 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Delete a file
 | 
			
		||||
	if msg.Event == config.EventFileDelete {
 | 
			
		||||
		if msg.ID == "" {
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if fi, ok := b.cache.Get(cFileUpload + msg.ID); ok {
 | 
			
		||||
			err := b.c.ChannelMessageDelete(channelID, fi.(string)) // nolint:forcetypeassert
 | 
			
		||||
			b.cache.Remove(cFileUpload + msg.ID)
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return "", fmt.Errorf("file %s not found", msg.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Upload a file if it exists
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		for _, rmsg := range helper.HandleExtra(msg, b.General) {
 | 
			
		||||
			rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped"))
 | 
			
		||||
		for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
			
		||||
			rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength)
 | 
			
		||||
			if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
 | 
			
		||||
				b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// check if we have files to upload (from slack, telegram or mattermost)
 | 
			
		||||
		if len(msg.Extra["file"]) > 0 {
 | 
			
		||||
			return b.handleUploadFile(msg, channelID)
 | 
			
		||||
			return b.handleUploadFile(&msg, channelID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
 | 
			
		||||
	msg.Text = helper.ClipMessage(msg.Text, MessageLength)
 | 
			
		||||
	msg.Text = b.replaceUserMentions(msg.Text)
 | 
			
		||||
 | 
			
		||||
	// Edit message
 | 
			
		||||
@@ -334,30 +296,57 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
 | 
			
		||||
		return msg.ID, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m := discordgo.MessageSend{
 | 
			
		||||
		Content:         msg.Username + msg.Text,
 | 
			
		||||
		AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.ParentValid() {
 | 
			
		||||
		m.Reference = &discordgo.MessageReference{
 | 
			
		||||
			MessageID: msg.ParentID,
 | 
			
		||||
			ChannelID: channelID,
 | 
			
		||||
			GuildID:   b.guildID,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post normal message
 | 
			
		||||
	res, err := b.c.ChannelMessageSendComplex(channelID, &m)
 | 
			
		||||
	res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return res.ID, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	return res.ID, nil
 | 
			
		||||
// useWebhook returns true if we have a webhook defined somewhere
 | 
			
		||||
func (b *Bdiscord) useWebhook() bool {
 | 
			
		||||
	if b.GetString("WebhookURL") != "" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.channelsMutex.RLock()
 | 
			
		||||
	defer b.channelsMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	for _, channel := range b.channelInfoMap {
 | 
			
		||||
		if channel.Options.WebhookURL != "" {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// isWebhookID returns true if the specified id is used in a defined webhook
 | 
			
		||||
func (b *Bdiscord) isWebhookID(id string) bool {
 | 
			
		||||
	if b.GetString("WebhookURL") != "" {
 | 
			
		||||
		wID, _ := b.splitURL(b.GetString("WebhookURL"))
 | 
			
		||||
		if wID == id {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.channelsMutex.RLock()
 | 
			
		||||
	defer b.channelsMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	for _, channel := range b.channelInfoMap {
 | 
			
		||||
		if channel.Options.WebhookURL != "" {
 | 
			
		||||
			wID, _ := b.splitURL(channel.Options.WebhookURL)
 | 
			
		||||
			if wID == id {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleUploadFile handles native upload of files
 | 
			
		||||
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{
 | 
			
		||||
@@ -366,19 +355,13 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
 | 
			
		||||
			Reader:      bytes.NewReader(*fi.Data),
 | 
			
		||||
		}
 | 
			
		||||
		m := discordgo.MessageSend{
 | 
			
		||||
			Content:         msg.Username + fi.Comment,
 | 
			
		||||
			Files:           []*discordgo.File{&file},
 | 
			
		||||
			AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
			Content: msg.Username + fi.Comment,
 | 
			
		||||
			Files:   []*discordgo.File{&file},
 | 
			
		||||
		}
 | 
			
		||||
		res, err := b.c.ChannelMessageSendComplex(channelID, &m)
 | 
			
		||||
		_, 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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,90 +3,32 @@ package bdiscord
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"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)
 | 
			
		||||
 | 
			
		||||
	if b.useChannelID {
 | 
			
		||||
		rmsg.Channel = "ID:" + m.ChannelID
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO(qaisjp): if other bridges support bulk deletions, it could be fanned out centrally
 | 
			
		||||
func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageDeleteBulk) { //nolint:unparam
 | 
			
		||||
	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,
 | 
			
		||||
			ID:      msgID,
 | 
			
		||||
			Event:   config.EventMsgDelete,
 | 
			
		||||
			Text:    config.EventMsgDelete,
 | 
			
		||||
			Channel: b.getChannelName(m.ChannelID),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
 | 
			
		||||
		b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
		b.Remote <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) 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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ignore our own typing messages
 | 
			
		||||
	if m.UserID == b.userID {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{Account: b.Account, Event: config.EventUserTyping}
 | 
			
		||||
	rmsg.Channel = b.getChannelName(m.ChannelID)
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam
 | 
			
		||||
	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 != nil {
 | 
			
		||||
	if m.Message.EditedTimestamp != "" {
 | 
			
		||||
		b.Log.Debugf("Sending edit message")
 | 
			
		||||
		m.Content += b.GetString("EditSuffix")
 | 
			
		||||
		msg := &discordgo.MessageCreate{
 | 
			
		||||
			Message: m.Message,
 | 
			
		||||
		}
 | 
			
		||||
		b.messageCreate(s, msg)
 | 
			
		||||
		b.messageCreate(s, (*discordgo.MessageCreate)(m))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@@ -94,7 +36,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// if using webhooks, do not relay if it's ours
 | 
			
		||||
	if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) {
 | 
			
		||||
	if b.useWebhook() && m.Author.Bot { // && b.isWebhookID(m.Author.ID) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -107,9 +49,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.stripCustomoji(m.Message.Content)
 | 
			
		||||
		m.Message.Content = b.replaceChannelMentions(m.Message.Content)
 | 
			
		||||
		rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -120,13 +62,16 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
 | 
			
		||||
 | 
			
		||||
	// set channel name
 | 
			
		||||
	rmsg.Channel = b.getChannelName(m.ChannelID)
 | 
			
		||||
	if b.useChannelID {
 | 
			
		||||
		rmsg.Channel = "ID:" + m.ChannelID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fromWebhook := m.WebhookID != ""
 | 
			
		||||
	if !fromWebhook && !b.GetBool("UseUserName") {
 | 
			
		||||
		rmsg.Username = b.getNick(m.Author, m.GuildID)
 | 
			
		||||
	// set username
 | 
			
		||||
	if !b.GetBool("UseUserName") {
 | 
			
		||||
		rmsg.Username = b.getNick(m.Author)
 | 
			
		||||
	} else {
 | 
			
		||||
		rmsg.Username = m.Author.Username
 | 
			
		||||
		if !fromWebhook && b.GetBool("UseDiscriminator") {
 | 
			
		||||
		if b.GetBool("UseDiscriminator") {
 | 
			
		||||
			rmsg.Username += "#" + m.Author.Discriminator
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -134,7 +79,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
 | 
			
		||||
	// if we have embedded content add it to text
 | 
			
		||||
	if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil {
 | 
			
		||||
		for _, embed := range m.Message.Embeds {
 | 
			
		||||
			rmsg.Text += handleEmbed(embed)
 | 
			
		||||
			rmsg.Text = rmsg.Text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -150,24 +95,12 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
 | 
			
		||||
		rmsg.Event = config.EventUserAction
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Replace emotes
 | 
			
		||||
	rmsg.Text = replaceEmotes(rmsg.Text)
 | 
			
		||||
 | 
			
		||||
	// Add our parent id if it exists, and if it's not referring to a message in another channel
 | 
			
		||||
	if ref := m.MessageReference; ref != nil && ref.ChannelID == m.ChannelID {
 | 
			
		||||
		rmsg.ParentID = ref.MessageID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
 | 
			
		||||
	if m.GuildID != b.guildID {
 | 
			
		||||
		b.Log.Debugf("Ignoring memberUpdate because it originates from a different guild")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if m.Member == nil {
 | 
			
		||||
		b.Log.Warnf("Received member update with no member information: %#v", m)
 | 
			
		||||
	}
 | 
			
		||||
@@ -195,13 +128,6 @@ 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
 | 
			
		||||
@@ -223,13 +149,6 @@ 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
 | 
			
		||||
@@ -249,33 +168,3 @@ func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRe
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleEmbed(embed *discordgo.MessageEmbed) string {
 | 
			
		||||
	var t []string
 | 
			
		||||
	var result string
 | 
			
		||||
 | 
			
		||||
	t = append(t, embed.Title)
 | 
			
		||||
	t = append(t, embed.Description)
 | 
			
		||||
	t = append(t, embed.URL)
 | 
			
		||||
 | 
			
		||||
	i := 0
 | 
			
		||||
	for _, e := range t {
 | 
			
		||||
		if e == "" {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		i++
 | 
			
		||||
		if i == 1 {
 | 
			
		||||
			result += " embed: " + e
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		result += " - " + e
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if result != "" {
 | 
			
		||||
		result += "\n"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
package bdiscord
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestHandleEmbed(t *testing.T) {
 | 
			
		||||
	testcases := map[string]struct {
 | 
			
		||||
		embed  *discordgo.MessageEmbed
 | 
			
		||||
		result string
 | 
			
		||||
	}{
 | 
			
		||||
		"allempty": {
 | 
			
		||||
			embed:  &discordgo.MessageEmbed{},
 | 
			
		||||
			result: "",
 | 
			
		||||
		},
 | 
			
		||||
		"one": {
 | 
			
		||||
			embed: &discordgo.MessageEmbed{
 | 
			
		||||
				Title: "blah",
 | 
			
		||||
			},
 | 
			
		||||
			result: " embed: blah\n",
 | 
			
		||||
		},
 | 
			
		||||
		"two": {
 | 
			
		||||
			embed: &discordgo.MessageEmbed{
 | 
			
		||||
				Title:       "blah",
 | 
			
		||||
				Description: "blah2",
 | 
			
		||||
			},
 | 
			
		||||
			result: " embed: blah - blah2\n",
 | 
			
		||||
		},
 | 
			
		||||
		"three": {
 | 
			
		||||
			embed: &discordgo.MessageEmbed{
 | 
			
		||||
				Title:       "blah",
 | 
			
		||||
				Description: "blah2",
 | 
			
		||||
				URL:         "blah3",
 | 
			
		||||
			},
 | 
			
		||||
			result: " embed: blah - blah2 - blah3\n",
 | 
			
		||||
		},
 | 
			
		||||
		"twob": {
 | 
			
		||||
			embed: &discordgo.MessageEmbed{
 | 
			
		||||
				Description: "blah2",
 | 
			
		||||
				URL:         "blah3",
 | 
			
		||||
			},
 | 
			
		||||
			result: " embed: blah2 - blah3\n",
 | 
			
		||||
		},
 | 
			
		||||
		"oneb": {
 | 
			
		||||
			embed: &discordgo.MessageEmbed{
 | 
			
		||||
				URL: "blah3",
 | 
			
		||||
			},
 | 
			
		||||
			result: " embed: blah3\n",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for name, tc := range testcases {
 | 
			
		||||
		assert.Equalf(t, tc.result, handleEmbed(tc.embed), "Testcases %s", name)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -9,31 +9,7 @@ import (
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) getAllowedMentions() *discordgo.MessageAllowedMentions {
 | 
			
		||||
	// If AllowMention is not specified, then allow all mentions (default Discord behavior)
 | 
			
		||||
	if !b.IsKeySet("AllowMention") {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Otherwise, allow only the mentions that are specified
 | 
			
		||||
	allowedMentionTypes := make([]discordgo.AllowedMentionType, 0, 3)
 | 
			
		||||
	for _, m := range b.GetStringSlice("AllowMention") {
 | 
			
		||||
		switch m {
 | 
			
		||||
		case "everyone":
 | 
			
		||||
			allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeEveryone)
 | 
			
		||||
		case "roles":
 | 
			
		||||
			allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeRoles)
 | 
			
		||||
		case "users":
 | 
			
		||||
			allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeUsers)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &discordgo.MessageAllowedMentions{
 | 
			
		||||
		Parse: allowedMentionTypes,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
 | 
			
		||||
func (b *Bdiscord) getNick(user *discordgo.User) string {
 | 
			
		||||
	b.membersMutex.RLock()
 | 
			
		||||
	defer b.membersMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
@@ -47,9 +23,9 @@ func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If we didn't find nick, search for it.
 | 
			
		||||
	member, err := b.c.GuildMember(guildID, user.ID)
 | 
			
		||||
	member, err := b.c.GuildMember(b.guildID, user.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Warnf("Failed to fetch information for member %#v on guild %#v: %s", user, guildID, err)
 | 
			
		||||
		b.Log.Warnf("Failed to fetch information for member %#v: %s", user, err)
 | 
			
		||||
		return user.Username
 | 
			
		||||
	} else if member == nil {
 | 
			
		||||
		b.Log.Warnf("Got no information for member %#v", user)
 | 
			
		||||
@@ -75,9 +51,6 @@ func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) getChannelID(name string) string {
 | 
			
		||||
	if strings.Contains(name, "/") {
 | 
			
		||||
		return b.getCategoryChannelID(name)
 | 
			
		||||
	}
 | 
			
		||||
	b.channelsMutex.RLock()
 | 
			
		||||
	defer b.channelsMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
@@ -86,92 +59,40 @@ func (b *Bdiscord) getChannelID(name string) string {
 | 
			
		||||
		return idcheck[1]
 | 
			
		||||
	}
 | 
			
		||||
	for _, channel := range b.channels {
 | 
			
		||||
		if channel.Name == name && channel.Type == discordgo.ChannelTypeGuildText {
 | 
			
		||||
		if channel.Name == name {
 | 
			
		||||
			return channel.ID
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) getCategoryChannelID(name string) string {
 | 
			
		||||
	b.channelsMutex.RLock()
 | 
			
		||||
	defer b.channelsMutex.RUnlock()
 | 
			
		||||
	res := strings.Split(name, "/")
 | 
			
		||||
	// shouldn't happen because function should be only called from getChannelID
 | 
			
		||||
	if len(res) != 2 {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	catName, chanName := res[0], res[1]
 | 
			
		||||
	for _, channel := range b.channels {
 | 
			
		||||
		// if we have a parentID, lookup the name of that parent (category)
 | 
			
		||||
		// and if it matches return it
 | 
			
		||||
		if channel.Name == chanName && channel.ParentID != "" {
 | 
			
		||||
			for _, cat := range b.channels {
 | 
			
		||||
				if cat.ID == channel.ParentID && cat.Name == catName {
 | 
			
		||||
					return channel.ID
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) getChannelName(id string) string {
 | 
			
		||||
	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
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, channel := range b.channels {
 | 
			
		||||
		if channel.ID == id {
 | 
			
		||||
			return b.getCategoryChannelName(channel.Name, channel.ParentID)
 | 
			
		||||
			return channel.Name
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) getCategoryChannelName(name, parentID string) string {
 | 
			
		||||
	var usesCat bool
 | 
			
		||||
	// do we have a category configuration in the channel config
 | 
			
		||||
	for _, c := range b.channelInfoMap {
 | 
			
		||||
		if strings.Contains(c.Name, "/") {
 | 
			
		||||
			usesCat = true
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// configuration without category, return the normal channel name
 | 
			
		||||
	if !usesCat {
 | 
			
		||||
		return name
 | 
			
		||||
	}
 | 
			
		||||
	// create a category/channel response
 | 
			
		||||
	for _, c := range b.channels {
 | 
			
		||||
		if c.ID == parentID {
 | 
			
		||||
			name = c.Name + "/" + name
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// See https://discordapp.com/developers/docs/reference#message-formatting.
 | 
			
		||||
	channelMentionRE = regexp.MustCompile("<#[0-9]+>")
 | 
			
		||||
	emojiRE          = regexp.MustCompile("<(:.*?:)[0-9]+>")
 | 
			
		||||
	userMentionRE    = regexp.MustCompile("@[^@\n]{1,32}")
 | 
			
		||||
	emoteRE          = regexp.MustCompile(`<a?(:\w+:)\d+>`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) replaceChannelMentions(text string) string {
 | 
			
		||||
	replaceChannelMentionFunc := func(match string) string {
 | 
			
		||||
		var err error
 | 
			
		||||
		channelID := match[2 : len(match)-1]
 | 
			
		||||
		channelName := b.getChannelName(channelID)
 | 
			
		||||
 | 
			
		||||
		channelName := b.getChannelName(channelID)
 | 
			
		||||
		// If we don't have the channel refresh our list.
 | 
			
		||||
		if channelName == "" {
 | 
			
		||||
			var err error
 | 
			
		||||
			b.channels, err = b.c.GuildChannels(b.guildID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return "#unknownchannel"
 | 
			
		||||
@@ -207,20 +128,19 @@ func (b *Bdiscord) replaceUserMentions(text string) string {
 | 
			
		||||
	return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func replaceEmotes(text string) string {
 | 
			
		||||
	return emoteRE.ReplaceAllString(text, "$1")
 | 
			
		||||
func (b *Bdiscord) stripCustomoji(text string) string {
 | 
			
		||||
	return emojiRE.ReplaceAllString(text, `$1`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) replaceAction(text string) (string, bool) {
 | 
			
		||||
	length := len(text)
 | 
			
		||||
	if length > 1 && text[0] == '_' && text[length-1] == '_' {
 | 
			
		||||
		return text[1 : length-1], true
 | 
			
		||||
	if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") {
 | 
			
		||||
		return text[1:], true
 | 
			
		||||
	}
 | 
			
		||||
	return text, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// splitURL splits a webhookURL and returns the ID and token.
 | 
			
		||||
func (b *Bdiscord) splitURL(url string) (string, string, bool) {
 | 
			
		||||
func (b *Bdiscord) splitURL(url string) (string, string) {
 | 
			
		||||
	const (
 | 
			
		||||
		expectedWebhookSplitCount = 7
 | 
			
		||||
		webhookIdxID              = 5
 | 
			
		||||
@@ -228,9 +148,9 @@ func (b *Bdiscord) splitURL(url string) (string, string, bool) {
 | 
			
		||||
	)
 | 
			
		||||
	webhookURLSplit := strings.Split(url, "/")
 | 
			
		||||
	if len(webhookURLSplit) != expectedWebhookSplitCount {
 | 
			
		||||
		return "", "", false
 | 
			
		||||
		b.Log.Fatalf("%s is no correct discord WebhookURL", url)
 | 
			
		||||
	}
 | 
			
		||||
	return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken], true
 | 
			
		||||
	return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func enumerateUsernames(s string) []string {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,257 +0,0 @@
 | 
			
		||||
// Package transmitter provides functionality for transmitting
 | 
			
		||||
// arbitrary webhook messages to Discord.
 | 
			
		||||
//
 | 
			
		||||
// The package provides the following functionality:
 | 
			
		||||
//
 | 
			
		||||
// - Creating new webhooks, whenever necessary
 | 
			
		||||
// - Loading webhooks that we have previously created
 | 
			
		||||
// - Sending new messages
 | 
			
		||||
// - Editing messages, via message ID
 | 
			
		||||
// - Deleting messages, via message ID
 | 
			
		||||
//
 | 
			
		||||
// The package has been designed for matterbridge, but with other
 | 
			
		||||
// Go bots in mind. The public API should be matterbridge-agnostic.
 | 
			
		||||
package transmitter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// A Transmitter represents a message manager for a single guild.
 | 
			
		||||
type Transmitter struct {
 | 
			
		||||
	session    *discordgo.Session
 | 
			
		||||
	guild      string
 | 
			
		||||
	title      string
 | 
			
		||||
	autoCreate bool
 | 
			
		||||
 | 
			
		||||
	// channelWebhooks maps from a channel ID to a webhook instance
 | 
			
		||||
	channelWebhooks map[string]*discordgo.Webhook
 | 
			
		||||
 | 
			
		||||
	mutex sync.RWMutex
 | 
			
		||||
 | 
			
		||||
	Log *log.Entry
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ErrWebhookNotFound is returned when a valid webhook for this channel/message combination does not exist
 | 
			
		||||
var ErrWebhookNotFound = errors.New("webhook for this channel and message does not exist")
 | 
			
		||||
 | 
			
		||||
// ErrPermissionDenied is returned if the bot does not have permission to manage webhooks.
 | 
			
		||||
//
 | 
			
		||||
// Bots can be granted a guild-wide permission and channel-specific permissions to manage webhooks.
 | 
			
		||||
// Despite potentially having guild-wide permission, channel specific overrides could deny a bot's permission to manage webhooks.
 | 
			
		||||
var ErrPermissionDenied = errors.New("missing 'Manage Webhooks' permission")
 | 
			
		||||
 | 
			
		||||
// New returns a new Transmitter given a Discord session, guild ID, and title.
 | 
			
		||||
func New(session *discordgo.Session, guild string, title string, autoCreate bool) *Transmitter {
 | 
			
		||||
	return &Transmitter{
 | 
			
		||||
		session:    session,
 | 
			
		||||
		guild:      guild,
 | 
			
		||||
		title:      title,
 | 
			
		||||
		autoCreate: autoCreate,
 | 
			
		||||
 | 
			
		||||
		channelWebhooks: make(map[string]*discordgo.Webhook),
 | 
			
		||||
 | 
			
		||||
		Log: log.NewEntry(log.StandardLogger()),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Send transmits a message to the given channel with the provided webhook data, and waits until Discord responds with message data.
 | 
			
		||||
func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) {
 | 
			
		||||
	wh, err := t.getOrCreateWebhook(channelID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg, err := t.session.WebhookExecute(wh.ID, wh.Token, true, params)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("execute failed: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return msg, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Edit will edit a message in a channel, if possible.
 | 
			
		||||
func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo.WebhookParams) error {
 | 
			
		||||
	wh := t.getWebhook(channelID)
 | 
			
		||||
 | 
			
		||||
	if wh == nil {
 | 
			
		||||
		return ErrWebhookNotFound
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uri := discordgo.EndpointWebhookToken(wh.ID, wh.Token) + "/messages/" + messageID
 | 
			
		||||
	_, err := t.session.RequestWithBucketID("PATCH", uri, params, discordgo.EndpointWebhookToken("", ""))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HasWebhook checks whether the transmitter is using a particular webhook.
 | 
			
		||||
func (t *Transmitter) HasWebhook(id string) bool {
 | 
			
		||||
	t.mutex.RLock()
 | 
			
		||||
	defer t.mutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	for _, wh := range t.channelWebhooks {
 | 
			
		||||
		if wh.ID == id {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddWebhook allows you to register a channel's webhook with the transmitter.
 | 
			
		||||
func (t *Transmitter) AddWebhook(channelID string, webhook *discordgo.Webhook) bool {
 | 
			
		||||
	t.Log.Debugf("Manually added webhook %#v to channel %#v", webhook.ID, channelID)
 | 
			
		||||
	t.mutex.Lock()
 | 
			
		||||
	defer t.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	_, replaced := t.channelWebhooks[channelID]
 | 
			
		||||
	t.channelWebhooks[channelID] = webhook
 | 
			
		||||
	return replaced
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RefreshGuildWebhooks loads "relevant" webhooks into the transmitter, with careful permission handling.
 | 
			
		||||
//
 | 
			
		||||
// Notes:
 | 
			
		||||
//
 | 
			
		||||
// - A webhook is "relevant" if it was created by this bot -- the ApplicationID should match the bot's ID.
 | 
			
		||||
// - The term "having permission" means having the "Manage Webhooks" permission. See ErrPermissionDenied for more information.
 | 
			
		||||
// - This function is additive and will not unload previously loaded webhooks.
 | 
			
		||||
// - A nil channelIDs slice is treated the same as an empty one.
 | 
			
		||||
//
 | 
			
		||||
// If the bot has guild-wide permission:
 | 
			
		||||
//
 | 
			
		||||
// 1. it will load any "relevant" webhooks from the entire guild
 | 
			
		||||
// 2. the given slice is ignored
 | 
			
		||||
//
 | 
			
		||||
// If the bot does not have guild-wide permission:
 | 
			
		||||
//
 | 
			
		||||
// 1. it will load any "relevant" webhooks in each channel
 | 
			
		||||
// 2. a single error will be returned if any error occurs (incl. if there is no permission for any of these channels)
 | 
			
		||||
//
 | 
			
		||||
// If any channel has more than one "relevant" webhook, it will randomly pick one.
 | 
			
		||||
func (t *Transmitter) RefreshGuildWebhooks(channelIDs []string) error {
 | 
			
		||||
	t.Log.Debugln("Refreshing guild webhooks")
 | 
			
		||||
 | 
			
		||||
	botID, err := getDiscordUserID(t.session)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("could not get current user: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get all existing webhooks
 | 
			
		||||
	hooks, err := t.session.GuildWebhooks(t.guild)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		switch {
 | 
			
		||||
		case isDiscordPermissionError(err):
 | 
			
		||||
			// We fallback on manually fetching hooks from individual channels
 | 
			
		||||
			// if we don't have the "Manage Webhooks" permission globally.
 | 
			
		||||
			// We can only do this if we were provided channelIDs, though.
 | 
			
		||||
			if len(channelIDs) == 0 {
 | 
			
		||||
				return ErrPermissionDenied
 | 
			
		||||
			}
 | 
			
		||||
			t.Log.Debugln("Missing global 'Manage Webhooks' permission, falling back on per-channel permission")
 | 
			
		||||
			return t.fetchChannelsHooks(channelIDs, botID)
 | 
			
		||||
		default:
 | 
			
		||||
			return fmt.Errorf("could not get webhooks: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Log.Debugln("Refreshing guild webhooks using global permission")
 | 
			
		||||
	t.assignHooksByAppID(hooks, botID, false)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// createWebhook creates a webhook for a specific channel.
 | 
			
		||||
func (t *Transmitter) createWebhook(channel string) (*discordgo.Webhook, error) {
 | 
			
		||||
	t.mutex.Lock()
 | 
			
		||||
	defer t.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	wh, err := t.session.WebhookCreate(channel, t.title+time.Now().Format(" 3:04:05PM"), "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.channelWebhooks[channel] = wh
 | 
			
		||||
	return wh, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Transmitter) getWebhook(channel string) *discordgo.Webhook {
 | 
			
		||||
	t.mutex.RLock()
 | 
			
		||||
	defer t.mutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	return t.channelWebhooks[channel]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Transmitter) getOrCreateWebhook(channelID string) (*discordgo.Webhook, error) {
 | 
			
		||||
	// If we have a webhook for this channel, immediately return it
 | 
			
		||||
	wh := t.getWebhook(channelID)
 | 
			
		||||
	if wh != nil {
 | 
			
		||||
		return wh, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Early exit if we don't want to automatically create one
 | 
			
		||||
	if !t.autoCreate {
 | 
			
		||||
		return nil, ErrWebhookNotFound
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Log.Infof("Creating a webhook for %s\n", channelID)
 | 
			
		||||
	wh, err := t.createWebhook(channelID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("could not create webhook: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return wh, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// fetchChannelsHooks fetches hooks for the given channelIDs and calls assignHooksByAppID for each channel's hooks
 | 
			
		||||
func (t *Transmitter) fetchChannelsHooks(channelIDs []string, botID string) error {
 | 
			
		||||
	// For each channel, search for relevant hooks
 | 
			
		||||
	var failedHooks []string
 | 
			
		||||
	for _, channelID := range channelIDs {
 | 
			
		||||
		hooks, err := t.session.ChannelWebhooks(channelID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			failedHooks = append(failedHooks, "\n- "+channelID+": "+err.Error())
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		t.assignHooksByAppID(hooks, botID, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Compose an error if any hooks failed
 | 
			
		||||
	if len(failedHooks) > 0 {
 | 
			
		||||
		return errors.New("failed to fetch hooks:" + strings.Join(failedHooks, ""))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Transmitter) assignHooksByAppID(hooks []*discordgo.Webhook, appID string, channelTargeted bool) {
 | 
			
		||||
	logLine := "Picking up webhook"
 | 
			
		||||
	if channelTargeted {
 | 
			
		||||
		logLine += " (channel targeted)"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.mutex.Lock()
 | 
			
		||||
	defer t.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	for _, wh := range hooks {
 | 
			
		||||
		if wh.ApplicationID != appID {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		t.channelWebhooks[wh.ChannelID] = wh
 | 
			
		||||
		t.Log.WithFields(log.Fields{
 | 
			
		||||
			"id":      wh.ID,
 | 
			
		||||
			"name":    wh.Name,
 | 
			
		||||
			"channel": wh.ChannelID,
 | 
			
		||||
		}).Println(logLine)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
package transmitter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions
 | 
			
		||||
func isDiscordPermissionError(err error) bool {
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	restErr, ok := err.(*discordgo.RESTError)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return restErr.Message != nil && restErr.Message.Code == discordgo.ErrCodeMissingPermissions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getDiscordUserID gets own user ID from state, and fallback on API request
 | 
			
		||||
func getDiscordUserID(session *discordgo.Session) (string, error) {
 | 
			
		||||
	if user := session.State.User; user != nil {
 | 
			
		||||
		return user.ID, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := session.User("@me")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return user.ID, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,148 +0,0 @@
 | 
			
		||||
package bdiscord
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks
 | 
			
		||||
func (b *Bdiscord) shouldMessageUseWebhooks(msg *config.Message) bool {
 | 
			
		||||
	if b.useAutoWebhooks {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.channelsMutex.RLock()
 | 
			
		||||
	defer b.channelsMutex.RUnlock()
 | 
			
		||||
	if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
 | 
			
		||||
		if ci.Options.WebhookURL != "" {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// maybeGetLocalAvatar checks if UseLocalAvatar contains the message's
 | 
			
		||||
// account or protocol, and if so, returns the Discord avatar (if exists)
 | 
			
		||||
func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
 | 
			
		||||
	for _, val := range b.GetStringSlice("UseLocalAvatar") {
 | 
			
		||||
		if msg.Protocol != val && msg.Account != val {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		member, err := b.getGuildMemberByNick(msg.Username)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return ""
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return member.User.AvatarURL("")
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// webhookSend send one or more message via webhook, taking care of file
 | 
			
		||||
// uploads (from slack, telegram or mattermost).
 | 
			
		||||
// Returns messageID and error.
 | 
			
		||||
func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		res *discordgo.Message
 | 
			
		||||
		err error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this)
 | 
			
		||||
	if msg.Avatar == "" {
 | 
			
		||||
		msg.Avatar = b.maybeGetLocalAvatar(msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// WebhookParams can have either `Content` or `File`.
 | 
			
		||||
 | 
			
		||||
	// We can't send empty messages.
 | 
			
		||||
	if msg.Text != "" {
 | 
			
		||||
		res, err = b.transmitter.Send(
 | 
			
		||||
			channelID,
 | 
			
		||||
			&discordgo.WebhookParams{
 | 
			
		||||
				Content:         msg.Text,
 | 
			
		||||
				Username:        msg.Username,
 | 
			
		||||
				AvatarURL:       msg.Avatar,
 | 
			
		||||
				AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		for _, f := range msg.Extra["file"] {
 | 
			
		||||
			fi := f.(config.FileInfo)
 | 
			
		||||
			file := discordgo.File{
 | 
			
		||||
				Name:        fi.Name,
 | 
			
		||||
				ContentType: "",
 | 
			
		||||
				Reader:      bytes.NewReader(*fi.Data),
 | 
			
		||||
			}
 | 
			
		||||
			content := fi.Comment
 | 
			
		||||
 | 
			
		||||
			_, e2 := b.transmitter.Send(
 | 
			
		||||
				channelID,
 | 
			
		||||
				&discordgo.WebhookParams{
 | 
			
		||||
					Username:        msg.Username,
 | 
			
		||||
					AvatarURL:       msg.Avatar,
 | 
			
		||||
					Files:           []*discordgo.File{&file},
 | 
			
		||||
					Content:         content,
 | 
			
		||||
					AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
				},
 | 
			
		||||
			)
 | 
			
		||||
			if e2 != nil {
 | 
			
		||||
				b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return res, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (string, error) {
 | 
			
		||||
	// skip events
 | 
			
		||||
	if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// skip empty messages
 | 
			
		||||
	if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) {
 | 
			
		||||
		b.Log.Debugf("Skipping empty message %#v", msg)
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
 | 
			
		||||
	msg.Text = b.replaceUserMentions(msg.Text)
 | 
			
		||||
	// discord username must be [0..32] max
 | 
			
		||||
	if len(msg.Username) > 32 {
 | 
			
		||||
		msg.Username = msg.Username[0:32]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.ID != "" {
 | 
			
		||||
		b.Log.Debugf("Editing webhook message")
 | 
			
		||||
		err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{
 | 
			
		||||
			Content:         msg.Text,
 | 
			
		||||
			Username:        msg.Username,
 | 
			
		||||
			AllowedMentions: b.getAllowedMentions(),
 | 
			
		||||
		})
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return msg.ID, nil
 | 
			
		||||
		}
 | 
			
		||||
		b.Log.Errorf("Could not edit webhook message: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Processing webhook sending for message %#v", msg)
 | 
			
		||||
	discordMsg, err := b.webhookSend(msg, channelID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msg, err)
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	if discordMsg == nil {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return discordMsg.ID, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,252 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@@ -14,10 +14,8 @@ import (
 | 
			
		||||
	"golang.org/x/image/webp"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/gomarkdown/markdown"
 | 
			
		||||
	"github.com/gomarkdown/markdown/html"
 | 
			
		||||
	"github.com/gomarkdown/markdown/parser"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"gitlab.com/golang-commonmark/markdown"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DownloadFile downloads the given non-authenticated URL.
 | 
			
		||||
@@ -48,30 +46,6 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) {
 | 
			
		||||
	return &data, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DownloadFileAuthRocket downloads the given URL using the specified Rocket user ID and authentication token.
 | 
			
		||||
func DownloadFileAuthRocket(url, token, userID string) (*[]byte, error) {
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
	client := &http.Client{
 | 
			
		||||
		Timeout: time.Second * 5,
 | 
			
		||||
	}
 | 
			
		||||
	req, err := http.NewRequest("GET", url, nil)
 | 
			
		||||
 | 
			
		||||
	req.Header.Add("X-Auth-Token", token)
 | 
			
		||||
	req.Header.Add("X-User-Id", userID)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	_, err = io.Copy(&buf, resp.Body)
 | 
			
		||||
	data := buf.Bytes()
 | 
			
		||||
	return &data, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSubLines splits messages in newline-delimited lines. If maxLineLength is
 | 
			
		||||
// specified as non-zero GetSubLines will also clip long lines to the maximum
 | 
			
		||||
// length and insert a warning marker that the line was clipped.
 | 
			
		||||
@@ -79,19 +53,11 @@ func DownloadFileAuthRocket(url, token, userID string) (*[]byte, error) {
 | 
			
		||||
// TODO: The current implementation has the inconvenient that it disregards
 | 
			
		||||
// word boundaries when splitting but this is hard to solve without potentially
 | 
			
		||||
// breaking formatting and other stylistic effects.
 | 
			
		||||
func GetSubLines(message string, maxLineLength int, clippingMessage string) []string {
 | 
			
		||||
	if clippingMessage == "" {
 | 
			
		||||
		clippingMessage = " <clipped message>"
 | 
			
		||||
	}
 | 
			
		||||
func GetSubLines(message string, maxLineLength int) []string {
 | 
			
		||||
	const clippingMessage = " <clipped message>"
 | 
			
		||||
 | 
			
		||||
	var lines []string
 | 
			
		||||
	for _, line := range strings.Split(strings.TrimSpace(message), "\n") {
 | 
			
		||||
		if line == "" {
 | 
			
		||||
			// Prevent sending empty messages, so we'll skip this line
 | 
			
		||||
			// if it has no content.
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if maxLineLength == 0 || len([]byte(line)) <= maxLineLength {
 | 
			
		||||
			lines = append(lines, line)
 | 
			
		||||
			continue
 | 
			
		||||
@@ -174,23 +140,17 @@ 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,
 | 
			
		||||
		NativeID: id,
 | 
			
		||||
		Name:    name,
 | 
			
		||||
		Data:    data,
 | 
			
		||||
		URL:     url,
 | 
			
		||||
		Comment: comment,
 | 
			
		||||
		Avatar:  avatar,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -204,11 +164,8 @@ func RemoveEmptyNewLines(msg string) string {
 | 
			
		||||
 | 
			
		||||
// ClipMessage trims a message to the specified length if it exceeds it and adds a warning
 | 
			
		||||
// to the message in case it does so.
 | 
			
		||||
func ClipMessage(text string, length int, clippingMessage string) string {
 | 
			
		||||
	if clippingMessage == "" {
 | 
			
		||||
		clippingMessage = " <clipped message>"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
func ClipMessage(text string, length int) string {
 | 
			
		||||
	const clippingMessage = " <clipped message>"
 | 
			
		||||
	if len(text) > length {
 | 
			
		||||
		text = text[:length-len(clippingMessage)]
 | 
			
		||||
		if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
 | 
			
		||||
@@ -219,21 +176,15 @@ func ClipMessage(text string, length int, clippingMessage string) string {
 | 
			
		||||
	return text
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
	markdownParser := parser.NewWithExtensions(extensions)
 | 
			
		||||
	renderer := html.NewRenderer(html.RendererOptions{
 | 
			
		||||
		Flags: 0,
 | 
			
		||||
	})
 | 
			
		||||
	parsedMarkdown := markdown.ToHTML([]byte(input), markdownParser, renderer)
 | 
			
		||||
	res := string(parsedMarkdown)
 | 
			
		||||
	md := markdown.New(markdown.XHTMLOutput(true), markdown.Breaks(true))
 | 
			
		||||
	res := md.RenderToString([]byte(input))
 | 
			
		||||
	res = strings.TrimPrefix(res, "<p>")
 | 
			
		||||
	res = strings.TrimSuffix(res, "</p>\n")
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ConvertWebPToPNG converts input data (which should be WebP format) to PNG format
 | 
			
		||||
// ConvertWebPToPNG convert input data (which should be WebP format to PNG format)
 | 
			
		||||
func ConvertWebPToPNG(data *[]byte) error {
 | 
			
		||||
	r := bytes.NewReader(*data)
 | 
			
		||||
	m, err := webp.Decode(r)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,96 +10,98 @@ import (
 | 
			
		||||
 | 
			
		||||
const testLineLength = 64
 | 
			
		||||
 | 
			
		||||
var lineSplittingTestCases = map[string]struct {
 | 
			
		||||
	input          string
 | 
			
		||||
	splitOutput    []string
 | 
			
		||||
	nonSplitOutput []string
 | 
			
		||||
}{
 | 
			
		||||
	"Short single-line message": {
 | 
			
		||||
		input:          "short",
 | 
			
		||||
		splitOutput:    []string{"short"},
 | 
			
		||||
		nonSplitOutput: []string{"short"},
 | 
			
		||||
	},
 | 
			
		||||
	"Long single-line message": {
 | 
			
		||||
		input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
 | 
			
		||||
		splitOutput: []string{
 | 
			
		||||
			"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
 | 
			
		||||
			"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
 | 
			
		||||
			" labore et dolore magna aliqua.",
 | 
			
		||||
var (
 | 
			
		||||
	lineSplittingTestCases = map[string]struct {
 | 
			
		||||
		input          string
 | 
			
		||||
		splitOutput    []string
 | 
			
		||||
		nonSplitOutput []string
 | 
			
		||||
	}{
 | 
			
		||||
		"Short single-line message": {
 | 
			
		||||
			input:          "short",
 | 
			
		||||
			splitOutput:    []string{"short"},
 | 
			
		||||
			nonSplitOutput: []string{"short"},
 | 
			
		||||
		},
 | 
			
		||||
		nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."},
 | 
			
		||||
	},
 | 
			
		||||
	"Short multi-line message": {
 | 
			
		||||
		input: "I\ncan't\nget\nno\nsatisfaction!",
 | 
			
		||||
		splitOutput: []string{
 | 
			
		||||
			"I",
 | 
			
		||||
			"can't",
 | 
			
		||||
			"get",
 | 
			
		||||
			"no",
 | 
			
		||||
			"satisfaction!",
 | 
			
		||||
		"Long single-line message": {
 | 
			
		||||
			input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
 | 
			
		||||
			splitOutput: []string{
 | 
			
		||||
				"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
 | 
			
		||||
				"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
 | 
			
		||||
				" labore et dolore magna aliqua.",
 | 
			
		||||
			},
 | 
			
		||||
			nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."},
 | 
			
		||||
		},
 | 
			
		||||
		nonSplitOutput: []string{
 | 
			
		||||
			"I",
 | 
			
		||||
			"can't",
 | 
			
		||||
			"get",
 | 
			
		||||
			"no",
 | 
			
		||||
			"satisfaction!",
 | 
			
		||||
		"Short multi-line message": {
 | 
			
		||||
			input: "I\ncan't\nget\nno\nsatisfaction!",
 | 
			
		||||
			splitOutput: []string{
 | 
			
		||||
				"I",
 | 
			
		||||
				"can't",
 | 
			
		||||
				"get",
 | 
			
		||||
				"no",
 | 
			
		||||
				"satisfaction!",
 | 
			
		||||
			},
 | 
			
		||||
			nonSplitOutput: []string{
 | 
			
		||||
				"I",
 | 
			
		||||
				"can't",
 | 
			
		||||
				"get",
 | 
			
		||||
				"no",
 | 
			
		||||
				"satisfaction!",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	"Long multi-line message": {
 | 
			
		||||
		input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
 | 
			
		||||
			"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
 | 
			
		||||
			"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" +
 | 
			
		||||
			"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
 | 
			
		||||
		splitOutput: []string{
 | 
			
		||||
			"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
 | 
			
		||||
			"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
 | 
			
		||||
			" labore et dolore magna aliqua.",
 | 
			
		||||
			"Ut enim ad minim veniam, quis nostrud exercita <clipped message>",
 | 
			
		||||
			"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>",
 | 
			
		||||
			"modo consequat.",
 | 
			
		||||
			"Duis aute irure dolor in reprehenderit in volu <clipped message>",
 | 
			
		||||
			"ptate velit esse cillum dolore eu fugiat nulla <clipped message>",
 | 
			
		||||
			" pariatur.",
 | 
			
		||||
			"Excepteur sint occaecat cupidatat non proident <clipped message>",
 | 
			
		||||
			", sunt in culpa qui officia deserunt mollit an <clipped message>",
 | 
			
		||||
			"im id est laborum.",
 | 
			
		||||
		"Long multi-line message": {
 | 
			
		||||
			input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
 | 
			
		||||
				"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
 | 
			
		||||
				"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" +
 | 
			
		||||
				"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
 | 
			
		||||
			splitOutput: []string{
 | 
			
		||||
				"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
 | 
			
		||||
				"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
 | 
			
		||||
				" labore et dolore magna aliqua.",
 | 
			
		||||
				"Ut enim ad minim veniam, quis nostrud exercita <clipped message>",
 | 
			
		||||
				"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>",
 | 
			
		||||
				"modo consequat.",
 | 
			
		||||
				"Duis aute irure dolor in reprehenderit in volu <clipped message>",
 | 
			
		||||
				"ptate velit esse cillum dolore eu fugiat nulla <clipped message>",
 | 
			
		||||
				" pariatur.",
 | 
			
		||||
				"Excepteur sint occaecat cupidatat non proident <clipped message>",
 | 
			
		||||
				", sunt in culpa qui officia deserunt mollit an <clipped message>",
 | 
			
		||||
				"im id est laborum.",
 | 
			
		||||
			},
 | 
			
		||||
			nonSplitOutput: []string{
 | 
			
		||||
				"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
 | 
			
		||||
				"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
 | 
			
		||||
				"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
 | 
			
		||||
				"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		nonSplitOutput: []string{
 | 
			
		||||
			"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
 | 
			
		||||
			"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
 | 
			
		||||
			"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
 | 
			
		||||
			"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
 | 
			
		||||
		"Message ending with new-line.": {
 | 
			
		||||
			input:          "Newline ending\n",
 | 
			
		||||
			splitOutput:    []string{"Newline ending"},
 | 
			
		||||
			nonSplitOutput: []string{"Newline ending"},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	"Message ending with new-line.": {
 | 
			
		||||
		input:          "Newline ending\n",
 | 
			
		||||
		splitOutput:    []string{"Newline ending"},
 | 
			
		||||
		nonSplitOutput: []string{"Newline ending"},
 | 
			
		||||
	},
 | 
			
		||||
	"Long message containing UTF-8 multi-byte runes": {
 | 
			
		||||
		input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說",
 | 
			
		||||
		splitOutput: []string{
 | 
			
		||||
			"不布人個我此而及單石業喜資富下 <clipped message>",
 | 
			
		||||
			"我河下日沒一我臺空達的常景便物 <clipped message>",
 | 
			
		||||
			"沒為……子大我別名解成?生賣的 <clipped message>",
 | 
			
		||||
			"全直黑,我自我結毛分洲了世當, <clipped message>",
 | 
			
		||||
			"是政福那是東;斯說",
 | 
			
		||||
		"Long message containing UTF-8 multi-byte runes": {
 | 
			
		||||
			input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說",
 | 
			
		||||
			splitOutput: []string{
 | 
			
		||||
				"不布人個我此而及單石業喜資富下 <clipped message>",
 | 
			
		||||
				"我河下日沒一我臺空達的常景便物 <clipped message>",
 | 
			
		||||
				"沒為……子大我別名解成?生賣的 <clipped message>",
 | 
			
		||||
				"全直黑,我自我結毛分洲了世當, <clipped message>",
 | 
			
		||||
				"是政福那是東;斯說",
 | 
			
		||||
			},
 | 
			
		||||
			nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
 | 
			
		||||
		},
 | 
			
		||||
		nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGetSubLines(t *testing.T) {
 | 
			
		||||
	for testname, testcase := range lineSplittingTestCases {
 | 
			
		||||
		splitLines := GetSubLines(testcase.input, testLineLength, "")
 | 
			
		||||
		splitLines := GetSubLines(testcase.input, testLineLength)
 | 
			
		||||
		assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname)
 | 
			
		||||
		for _, splitLine := range splitLines {
 | 
			
		||||
			byteLength := len([]byte(splitLine))
 | 
			
		||||
			assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		nonSplitLines := GetSubLines(testcase.input, 0, "")
 | 
			
		||||
		nonSplitLines := GetSubLines(testcase.input, 0)
 | 
			
		||||
		assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -108,19 +110,16 @@ func TestConvertWebPToPNG(t *testing.T) {
 | 
			
		||||
	if os.Getenv("LOCAL_TEST") == "" {
 | 
			
		||||
		t.Skip()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	input, err := ioutil.ReadFile("test.webp")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fail()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d := &input
 | 
			
		||||
	err = ConvertWebPToPNG(d)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fail()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = ioutil.WriteFile("test.png", *d, 0o644) // nolint:gosec
 | 
			
		||||
	err = ioutil.WriteFile("test.png", *d, 0644)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fail()
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
//go:build cgolottie
 | 
			
		||||
 | 
			
		||||
package helper
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/Benau/tgsconverter/libtgsconverter"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func CanConvertTgsToX() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ConvertTgsToX convert input data (which should be tgs format) to any format supported by libtgsconverter
 | 
			
		||||
func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
 | 
			
		||||
	options := libtgsconverter.NewConverterOptions()
 | 
			
		||||
	options.SetExtension(outputFormat)
 | 
			
		||||
	blob, err := libtgsconverter.ImportFromData(*data, options)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to run libtgsconverter.ImportFromData: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*data = blob
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SupportsFormat(format string) bool {
 | 
			
		||||
	return libtgsconverter.SupportsExtension(format)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LottieBackend() string {
 | 
			
		||||
	return "libtgsconverter"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,90 +0,0 @@
 | 
			
		||||
//go:build !cgolottie
 | 
			
		||||
 | 
			
		||||
package helper
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CanConvertTgsToX Checks whether the external command necessary for ConvertTgsToX works.
 | 
			
		||||
func CanConvertTgsToX() error {
 | 
			
		||||
	// We depend on the fact that `lottie_convert.py --help` has exit status 0.
 | 
			
		||||
	// Hyrum's Law predicted this, and Murphy's Law predicts that this will break eventually.
 | 
			
		||||
	// However, there is no alternative like `lottie_convert.py --is-properly-installed`
 | 
			
		||||
	cmd := exec.Command("lottie_convert.py", "--help")
 | 
			
		||||
	return cmd.Run()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ConvertTgsToWebP convert input data (which should be tgs format) to WebP format
 | 
			
		||||
// This relies on an external command, which is ugly, but works.
 | 
			
		||||
func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
 | 
			
		||||
	// lottie can't handle input from a pipe, so write to a temporary file:
 | 
			
		||||
	tmpInFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-input-*.tgs")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	tmpInFileName := tmpInFile.Name()
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if removeErr := os.Remove(tmpInFileName); removeErr != nil {
 | 
			
		||||
			logger.Errorf("Could not delete temporary (input) file %s: %v", tmpInFileName, removeErr)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	// lottie can handle writing to a pipe, but there is no way to do that platform-independently.
 | 
			
		||||
	// "/dev/stdout" won't work on Windows, and "-" upsets Cairo for some reason. So we need another file:
 | 
			
		||||
	tmpOutFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-output-*.data")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	tmpOutFileName := tmpOutFile.Name()
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if removeErr := os.Remove(tmpOutFileName); removeErr != nil {
 | 
			
		||||
			logger.Errorf("Could not delete temporary (output) file %s: %v", tmpOutFileName, removeErr)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if _, writeErr := tmpInFile.Write(*data); writeErr != nil {
 | 
			
		||||
		return writeErr
 | 
			
		||||
	}
 | 
			
		||||
	// Must close before calling lottie to avoid data races:
 | 
			
		||||
	if closeErr := tmpInFile.Close(); closeErr != nil {
 | 
			
		||||
		return closeErr
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Call lottie to transform:
 | 
			
		||||
	cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpInFileName, tmpOutFileName)
 | 
			
		||||
	cmd.Stdout = nil
 | 
			
		||||
	cmd.Stderr = nil
 | 
			
		||||
	// NB: lottie writes progress into to stderr in all cases.
 | 
			
		||||
	_, stderr := cmd.Output()
 | 
			
		||||
	if stderr != nil {
 | 
			
		||||
		// 'stderr' already contains some parts of Stderr, because it was set to 'nil'.
 | 
			
		||||
		return stderr
 | 
			
		||||
	}
 | 
			
		||||
	dataContents, err := ioutil.ReadFile(tmpOutFileName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*data = dataContents
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SupportsFormat(format string) bool {
 | 
			
		||||
	switch format {
 | 
			
		||||
	case "png":
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case "webp":
 | 
			
		||||
		return true
 | 
			
		||||
	default:
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LottieBackend() string {
 | 
			
		||||
	return "lottie_convert.py"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@@ -4,12 +4,14 @@ import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/dfordsoft/golib/ic"
 | 
			
		||||
	"github.com/lrstanley/girc"
 | 
			
		||||
	"github.com/paulrosania/go-charset/charset"
 | 
			
		||||
	"github.com/saintfish/chardet"
 | 
			
		||||
@@ -23,12 +25,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 = toUTF8(b.GetString("Charset"), msg.Text)
 | 
			
		||||
			msg.Text = ic.ConvertString("utf-8", 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 to utf-8 conversion failed: %s", err)
 | 
			
		||||
				b.Log.Errorf("charset from utf-8 conversion failed: %s", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Fprint(w, msg.Text)
 | 
			
		||||
@@ -53,12 +55,12 @@ func (b *Birc) handleFiles(msg *config.Message) bool {
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
		if fi.Comment != "" {
 | 
			
		||||
			msg.Text += fi.Comment + " : "
 | 
			
		||||
			msg.Text += fi.Comment + ": "
 | 
			
		||||
		}
 | 
			
		||||
		if fi.URL != "" {
 | 
			
		||||
			msg.Text = fi.URL
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				msg.Text = fi.Comment + " : " + fi.URL
 | 
			
		||||
				msg.Text = fi.Comment + ": " + fi.URL
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
 | 
			
		||||
@@ -66,20 +68,6 @@ func (b *Birc) handleFiles(msg *config.Message) bool {
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Birc) handleInvite(client *girc.Client, event girc.Event) {
 | 
			
		||||
	if len(event.Params) != 2 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	channel := event.Params[1]
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("got invite for %s", channel)
 | 
			
		||||
 | 
			
		||||
	if _, ok := b.channels[channel]; ok {
 | 
			
		||||
		b.i.Cmd.Join(channel)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
 | 
			
		||||
	if len(event.Params) == 0 {
 | 
			
		||||
		b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
 | 
			
		||||
@@ -103,13 +91,8 @@ func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
 | 
			
		||||
		if b.GetBool("nosendjoinpart") {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account)
 | 
			
		||||
		msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
 | 
			
		||||
		if b.GetBool("verbosejoinpart") {
 | 
			
		||||
			b.Log.Debugf("<= Sending verbose JOIN_LEAVE event from %s to gateway", b.Account)
 | 
			
		||||
			msg = config.Message{Username: "system", Text: event.Source.Name + " (" + event.Source.Ident + "@" + event.Source.Host + ") " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
 | 
			
		||||
		} else {
 | 
			
		||||
			b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account)
 | 
			
		||||
		}
 | 
			
		||||
		b.Log.Debugf("<= Message is %#v", msg)
 | 
			
		||||
		b.Remote <- msg
 | 
			
		||||
		return
 | 
			
		||||
@@ -122,15 +105,14 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
 | 
			
		||||
	i := b.i
 | 
			
		||||
	b.Nick = event.Params[0]
 | 
			
		||||
 | 
			
		||||
	i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg)
 | 
			
		||||
	i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg)
 | 
			
		||||
	i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
 | 
			
		||||
	i.Handlers.Add("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)
 | 
			
		||||
	i.Handlers.AddBg("PART", b.handleJoinPart)
 | 
			
		||||
	i.Handlers.AddBg("QUIT", b.handleJoinPart)
 | 
			
		||||
	i.Handlers.AddBg("KICK", b.handleJoinPart)
 | 
			
		||||
	i.Handlers.Add("INVITE", b.handleInvite)
 | 
			
		||||
	i.Handlers.Add(girc.NOTICE, b.handleNotice)
 | 
			
		||||
	i.Handlers.Add("JOIN", b.handleJoinPart)
 | 
			
		||||
	i.Handlers.Add("PART", b.handleJoinPart)
 | 
			
		||||
	i.Handlers.Add("QUIT", b.handleJoinPart)
 | 
			
		||||
	i.Handlers.Add("KICK", b.handleJoinPart)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Birc) handleNickServ() {
 | 
			
		||||
@@ -184,14 +166,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
 | 
			
		||||
	if b.skipPrivMsg(event) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		Username: event.Source.Name,
 | 
			
		||||
		Channel:  strings.ToLower(event.Params[0]),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		UserID:   event.Source.Ident + "@" + event.Source.Host,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
 | 
			
		||||
	b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
 | 
			
		||||
 | 
			
		||||
	// set action event
 | 
			
		||||
@@ -199,14 +174,13 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
 | 
			
		||||
		rmsg.Event = config.EventUserAction
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// set NOTICE event
 | 
			
		||||
	if event.Command == "NOTICE" {
 | 
			
		||||
		rmsg.Event = config.EventNoticeIRC
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// strip action, we made an event if it was an action
 | 
			
		||||
	rmsg.Text += event.StripAction()
 | 
			
		||||
 | 
			
		||||
	// strip IRC colors
 | 
			
		||||
	re := regexp.MustCompile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
 | 
			
		||||
	rmsg.Text = re.ReplaceAllString(rmsg.Text, "")
 | 
			
		||||
 | 
			
		||||
	// start detecting the charset
 | 
			
		||||
	mycharset := b.GetString("Charset")
 | 
			
		||||
	if mycharset == "" {
 | 
			
		||||
@@ -226,7 +200,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 = toUTF8(b.GetString("Charset"), rmsg.Text)
 | 
			
		||||
		rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text)
 | 
			
		||||
	default:
 | 
			
		||||
		r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -243,7 +217,6 @@ 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,10 +2,8 @@ package birc
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"hash/crc32"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strconv"
 | 
			
		||||
@@ -16,7 +14,6 @@ import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/lrstanley/girc"
 | 
			
		||||
	stripmd "github.com/writeas/go-strip-markdown"
 | 
			
		||||
 | 
			
		||||
	// We need to import the 'data' package as an implicit dependency.
 | 
			
		||||
	// See: https://godoc.org/github.com/paulrosania/go-charset/charset
 | 
			
		||||
@@ -31,7 +28,6 @@ type Birc struct {
 | 
			
		||||
	Local                                     chan config.Message // local queue for flood control
 | 
			
		||||
	FirstConnection, authDone                 bool
 | 
			
		||||
	MessageDelay, MessageQueue, MessageLength int
 | 
			
		||||
	channels                                  map[string]bool
 | 
			
		||||
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
}
 | 
			
		||||
@@ -42,8 +38,6 @@ func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	b.Nick = b.GetString("Nick")
 | 
			
		||||
	b.names = make(map[string][]string)
 | 
			
		||||
	b.connected = make(chan error)
 | 
			
		||||
	b.channels = make(map[string]bool)
 | 
			
		||||
 | 
			
		||||
	if b.GetInt("MessageDelay") == 0 {
 | 
			
		||||
		b.MessageDelay = 1300
 | 
			
		||||
	} else {
 | 
			
		||||
@@ -73,10 +67,6 @@ 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"))
 | 
			
		||||
 | 
			
		||||
@@ -120,7 +110,6 @@ func (b *Birc) Disconnect() error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	b.channels[channel.Name] = true
 | 
			
		||||
	// need to check if we have nickserv auth done before joining channels
 | 
			
		||||
	for {
 | 
			
		||||
		if b.authDone {
 | 
			
		||||
@@ -167,14 +156,10 @@ func (b *Birc) Send(msg config.Message) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var msgLines []string
 | 
			
		||||
	if b.GetBool("StripMarkdown") {
 | 
			
		||||
		msg.Text = stripmd.Strip(msg.Text)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.GetBool("MessageSplit") {
 | 
			
		||||
		msgLines = helper.GetSubLines(msg.Text, b.MessageLength, b.GetString("MessageClipped"))
 | 
			
		||||
		msgLines = helper.GetSubLines(msg.Text, b.MessageLength)
 | 
			
		||||
	} else {
 | 
			
		||||
		msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
 | 
			
		||||
		msgLines = helper.GetSubLines(msg.Text, 0)
 | 
			
		||||
	}
 | 
			
		||||
	for i := range msgLines {
 | 
			
		||||
		if len(b.Local) >= b.MessageQueue {
 | 
			
		||||
@@ -182,8 +167,12 @@ func (b *Birc) Send(msg config.Message) (string, error) {
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		msg.Text = msgLines[i]
 | 
			
		||||
		b.Local <- msg
 | 
			
		||||
		b.Local <- config.Message{
 | 
			
		||||
			Text:     msgLines[i],
 | 
			
		||||
			Username: msg.Username,
 | 
			
		||||
			Channel:  msg.Channel,
 | 
			
		||||
			Event:    msg.Event,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
@@ -210,58 +199,22 @@ func (b *Birc) doConnect() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sanitize nicks for RELAYMSG: replace IRC characters with special meanings with "-"
 | 
			
		||||
func sanitizeNick(nick string) string {
 | 
			
		||||
	sanitize := func(r rune) rune {
 | 
			
		||||
		if strings.ContainsRune("!+%@&#$:'\"?*,. ", r) {
 | 
			
		||||
			return '-'
 | 
			
		||||
		}
 | 
			
		||||
		return r
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Map(sanitize, nick)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Birc) doSend() {
 | 
			
		||||
	rate := time.Millisecond * time.Duration(b.MessageDelay)
 | 
			
		||||
	throttle := time.NewTicker(rate)
 | 
			
		||||
	for msg := range b.Local {
 | 
			
		||||
		<-throttle.C
 | 
			
		||||
		username := msg.Username
 | 
			
		||||
		// Optional support for the proposed RELAYMSG extension, described at
 | 
			
		||||
		// https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md
 | 
			
		||||
		// nolint:nestif
 | 
			
		||||
		if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) &&
 | 
			
		||||
			b.GetBool("UseRelayMsg") {
 | 
			
		||||
			username = sanitizeNick(username)
 | 
			
		||||
			text := msg.Text
 | 
			
		||||
 | 
			
		||||
			// Work around girc chomping leading commas on single word messages?
 | 
			
		||||
			if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') {
 | 
			
		||||
				text = ":" + text
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if msg.Event == config.EventUserAction {
 | 
			
		||||
				b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck
 | 
			
		||||
			} else {
 | 
			
		||||
				b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username)
 | 
			
		||||
				b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck
 | 
			
		||||
			}
 | 
			
		||||
		if b.GetBool("Colornicks") {
 | 
			
		||||
			checksum := crc32.ChecksumIEEE([]byte(msg.Username))
 | 
			
		||||
			colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
 | 
			
		||||
			username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
 | 
			
		||||
		}
 | 
			
		||||
		if msg.Event == config.EventUserAction {
 | 
			
		||||
			b.i.Cmd.Action(msg.Channel, username+msg.Text)
 | 
			
		||||
		} else {
 | 
			
		||||
			if b.GetBool("Colornicks") {
 | 
			
		||||
				checksum := crc32.ChecksumIEEE([]byte(msg.Username))
 | 
			
		||||
				colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
 | 
			
		||||
				username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
 | 
			
		||||
			}
 | 
			
		||||
			switch msg.Event {
 | 
			
		||||
			case config.EventUserAction:
 | 
			
		||||
				b.i.Cmd.Action(msg.Channel, username+msg.Text)
 | 
			
		||||
			case config.EventNoticeIRC:
 | 
			
		||||
				b.Log.Debugf("Sending notice to channel %s", msg.Channel)
 | 
			
		||||
				b.i.Cmd.Notice(msg.Channel, username+msg.Text)
 | 
			
		||||
			default:
 | 
			
		||||
				b.Log.Debugf("Sending to channel %s", msg.Channel)
 | 
			
		||||
				b.i.Cmd.Message(msg.Channel, username+msg.Text)
 | 
			
		||||
			}
 | 
			
		||||
			b.Log.Debugf("Sending to channel %s", msg.Channel)
 | 
			
		||||
			b.i.Cmd.Message(msg.Channel, username+msg.Text)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -276,11 +229,8 @@ func (b *Birc) getClient() (*girc.Client, error) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	user := b.GetString("UserName")
 | 
			
		||||
	if user == "" {
 | 
			
		||||
		user = b.GetString("Nick")
 | 
			
		||||
	}
 | 
			
		||||
	// fix strict user handling of girc
 | 
			
		||||
	user := b.GetString("Nick")
 | 
			
		||||
	for !girc.IsValidUser(user) {
 | 
			
		||||
		if len(user) == 1 || len(user) == 0 {
 | 
			
		||||
			user = "matterbridge"
 | 
			
		||||
@@ -288,27 +238,6 @@ func (b *Birc) getClient() (*girc.Client, error) {
 | 
			
		||||
		}
 | 
			
		||||
		user = user[1:]
 | 
			
		||||
	}
 | 
			
		||||
	realName := b.GetString("RealName")
 | 
			
		||||
	if realName == "" {
 | 
			
		||||
		realName = b.GetString("Nick")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	debug := ioutil.Discard
 | 
			
		||||
	if b.GetInt("DebugLevel") == 2 {
 | 
			
		||||
		debug = b.Log.Writer()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pingDelay, err := time.ParseDuration(b.GetString("pingdelay"))
 | 
			
		||||
	if err != nil || pingDelay == 0 {
 | 
			
		||||
		pingDelay = time.Minute
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("setting pingdelay to %s", pingDelay)
 | 
			
		||||
 | 
			
		||||
	tlsConfig, err := b.getTLSConfig()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	i := girc.New(girc.Config{
 | 
			
		||||
		Server:     server,
 | 
			
		||||
@@ -316,15 +245,10 @@ func (b *Birc) getClient() (*girc.Client, error) {
 | 
			
		||||
		Port:       port,
 | 
			
		||||
		Nick:       b.GetString("Nick"),
 | 
			
		||||
		User:       user,
 | 
			
		||||
		Name:       realName,
 | 
			
		||||
		Name:       b.GetString("Nick"),
 | 
			
		||||
		SSL:        b.GetBool("UseTLS"),
 | 
			
		||||
		Bind:       b.GetString("Bind"),
 | 
			
		||||
		TLSConfig:  tlsConfig,
 | 
			
		||||
		PingDelay:  pingDelay,
 | 
			
		||||
		// skip gIRC internal rate limiting, since we have our own throttling
 | 
			
		||||
		AllowFlood:    true,
 | 
			
		||||
		Debug:         debug,
 | 
			
		||||
		SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil},
 | 
			
		||||
		TLSConfig:  &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec
 | 
			
		||||
		PingDelay:  time.Minute,
 | 
			
		||||
	})
 | 
			
		||||
	return i, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -334,16 +258,12 @@ func (b *Birc) endNames(client *girc.Client, event girc.Event) {
 | 
			
		||||
	sort.Strings(b.names[channel])
 | 
			
		||||
	maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
 | 
			
		||||
	for len(b.names[channel]) > maxNamesPerPost {
 | 
			
		||||
		b.Remote <- config.Message{
 | 
			
		||||
			Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]),
 | 
			
		||||
			Channel: channel, Account: b.Account,
 | 
			
		||||
		}
 | 
			
		||||
		b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]),
 | 
			
		||||
			Channel: channel, Account: b.Account}
 | 
			
		||||
		b.names[channel] = b.names[channel][maxNamesPerPost:]
 | 
			
		||||
	}
 | 
			
		||||
	b.Remote <- config.Message{
 | 
			
		||||
		Username: b.Nick, Text: b.formatnicks(b.names[channel]),
 | 
			
		||||
		Channel: channel, Account: b.Account,
 | 
			
		||||
	}
 | 
			
		||||
	b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel]),
 | 
			
		||||
		Channel: channel, Account: b.Account}
 | 
			
		||||
	b.names[channel] = nil
 | 
			
		||||
	b.i.Handlers.Clear(girc.RPL_NAMREPLY)
 | 
			
		||||
	b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
 | 
			
		||||
@@ -354,7 +274,7 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
 | 
			
		||||
	b.Nick = b.i.GetNick()
 | 
			
		||||
 | 
			
		||||
	// freenode doesn't send 001 as first reply
 | 
			
		||||
	if event.Command == "NOTICE" && len(event.Params) != 2 {
 | 
			
		||||
	if event.Command == "NOTICE" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	// don't forward queries to the bot
 | 
			
		||||
@@ -362,18 +282,7 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	// don't forward message from ourself
 | 
			
		||||
	if event.Source != nil {
 | 
			
		||||
		if event.Source.Name == b.Nick {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// don't forward messages we sent via RELAYMSG
 | 
			
		||||
	if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	// This is the old name of the cap sent in spoofed messages; I've kept this in
 | 
			
		||||
	// for compatibility reasons
 | 
			
		||||
	if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick {
 | 
			
		||||
	if event.Source.Name == b.Nick {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
@@ -393,23 +302,3 @@ 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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
package bkeybase
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/keybase/go-keybase-chat-bot/kbchat/types/chat1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Bkeybase) handleKeybase() {
 | 
			
		||||
	sub, err := b.kbc.ListenForNewTextMessages()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Error listening: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			msg, err := sub.Read()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Errorf("failed to read message: %s", err.Error())
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if msg.Message.Content.TypeName != "text" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if msg.Message.Sender.Username == b.kbc.GetUsername() {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			b.handleMessage(msg.Message)
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bkeybase) handleMessage(msg chat1.MsgSummary) {
 | 
			
		||||
	b.Log.Debugf("== Receiving event: %#v", msg)
 | 
			
		||||
	if msg.Channel.TopicName != b.channel || msg.Channel.Name != b.team {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.Sender.Username != b.kbc.GetUsername() {
 | 
			
		||||
 | 
			
		||||
		// TODO download avatar
 | 
			
		||||
 | 
			
		||||
		// Create our message
 | 
			
		||||
		rmsg := config.Message{Username: msg.Sender.Username, Text: msg.Content.Text.Body, UserID: string(msg.Sender.Uid), Channel: msg.Channel.TopicName, ID: strconv.Itoa(int(msg.Id)), Account: b.Account}
 | 
			
		||||
 | 
			
		||||
		// Text must be a string
 | 
			
		||||
		if msg.Content.TypeName != "text" {
 | 
			
		||||
			b.Log.Errorf("message is not text")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Log.Debugf("<= Sending message from %s on %s to gateway", msg.Sender.Username, msg.Channel.Name)
 | 
			
		||||
		b.Remote <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,106 +0,0 @@
 | 
			
		||||
package bkeybase
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/keybase/go-keybase-chat-bot/kbchat"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Bkeybase bridge structure
 | 
			
		||||
type Bkeybase struct {
 | 
			
		||||
	kbc     *kbchat.API
 | 
			
		||||
	user    string
 | 
			
		||||
	channel string
 | 
			
		||||
	team    string
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New initializes Bkeybase object and sets team
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	b := &Bkeybase{Config: cfg}
 | 
			
		||||
	b.team = b.Config.GetString("Team")
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Connect starts keybase API and listener loop
 | 
			
		||||
func (b *Bkeybase) Connect() error {
 | 
			
		||||
	var err error
 | 
			
		||||
	b.Log.Infof("Connecting %s", b.GetString("Team"))
 | 
			
		||||
 | 
			
		||||
	// use default keybase location (`keybase`)
 | 
			
		||||
	b.kbc, err = kbchat.Start(kbchat.RunOptions{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.user = b.kbc.GetUsername()
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	go b.handleKeybase()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Disconnect doesn't do anything for now
 | 
			
		||||
func (b *Bkeybase) Disconnect() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JoinChannel sets channel name in struct
 | 
			
		||||
func (b *Bkeybase) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	if _, err := b.kbc.JoinChannel(b.team, channel.Name); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.channel = channel.Name
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Send receives bridge messages and sends them to Keybase chat room
 | 
			
		||||
func (b *Bkeybase) Send(msg config.Message) (string, error) {
 | 
			
		||||
	b.Log.Debugf("=> Receiving %#v", msg)
 | 
			
		||||
 | 
			
		||||
	// Handle /me events
 | 
			
		||||
	if msg.Event == config.EventUserAction {
 | 
			
		||||
		msg.Text = "_" + msg.Text + "_"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Delete message if we have an ID
 | 
			
		||||
	// Delete message not supported by keybase go library yet
 | 
			
		||||
 | 
			
		||||
	// Edit message if we have an ID
 | 
			
		||||
	// kbchat lib does not support message editing yet
 | 
			
		||||
 | 
			
		||||
	if len(msg.Extra["file"]) > 0 {
 | 
			
		||||
		// Upload a file
 | 
			
		||||
		dir, err := ioutil.TempDir("", "matterbridge")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		defer os.RemoveAll(dir)
 | 
			
		||||
 | 
			
		||||
		for _, f := range msg.Extra["file"] {
 | 
			
		||||
			fname := f.(config.FileInfo).Name
 | 
			
		||||
			fdata := *f.(config.FileInfo).Data
 | 
			
		||||
			fcaption := f.(config.FileInfo).Comment
 | 
			
		||||
			fpath := filepath.Join(dir, fname)
 | 
			
		||||
 | 
			
		||||
			if err = ioutil.WriteFile(fpath, fdata, 0600); err != nil {
 | 
			
		||||
				return "", err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			_, _ = b.kbc.SendAttachmentByTeam(b.team, &b.channel, fpath, fcaption)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Send regular message
 | 
			
		||||
	text := msg.Username + msg.Text
 | 
			
		||||
	resp, err := b.kbc.SendMessageByTeamName(b.team, &b.channel, text)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return strconv.Itoa(int(*resp.Result.MessageID)), err
 | 
			
		||||
}
 | 
			
		||||
@@ -1,215 +0,0 @@
 | 
			
		||||
package bmatrix
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	matrix "github.com/matterbridge/gomatrix"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func newMatrixUsername(username string) *matrixUsername {
 | 
			
		||||
	mUsername := new(matrixUsername)
 | 
			
		||||
 | 
			
		||||
	// check if we have a </tag>. if we have, we don't escape HTML. #696
 | 
			
		||||
	if htmlTag.MatchString(username) {
 | 
			
		||||
		mUsername.formatted = username
 | 
			
		||||
		// remove the HTML formatting for beautiful push messages #1188
 | 
			
		||||
		mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "")
 | 
			
		||||
	} else {
 | 
			
		||||
		mUsername.formatted = html.EscapeString(username)
 | 
			
		||||
		mUsername.plain = username
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return mUsername
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getRoomID retrieves a matching room ID from the channel name.
 | 
			
		||||
func (b *Bmatrix) getRoomID(channel string) string {
 | 
			
		||||
	b.RLock()
 | 
			
		||||
	defer b.RUnlock()
 | 
			
		||||
	for ID, name := range b.RoomMap {
 | 
			
		||||
		if name == channel {
 | 
			
		||||
			return ID
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// interface2Struct marshals and immediately unmarshals an interface.
 | 
			
		||||
// Useful for converting map[string]interface{} to a struct.
 | 
			
		||||
func interface2Struct(in interface{}, out interface{}) error {
 | 
			
		||||
	jsonObj, err := json.Marshal(in)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err //nolint:wrapcheck
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return json.Unmarshal(jsonObj, out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache.
 | 
			
		||||
func (b *Bmatrix) getDisplayName(mxid string) string {
 | 
			
		||||
	if b.GetBool("UseUserName") {
 | 
			
		||||
		return mxid[1:]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.RLock()
 | 
			
		||||
	if val, present := b.NicknameMap[mxid]; present {
 | 
			
		||||
		b.RUnlock()
 | 
			
		||||
 | 
			
		||||
		return val.displayName
 | 
			
		||||
	}
 | 
			
		||||
	b.RUnlock()
 | 
			
		||||
 | 
			
		||||
	displayName, err := b.mc.GetDisplayName(mxid)
 | 
			
		||||
	var httpError *matrix.HTTPError
 | 
			
		||||
	if errors.As(err, &httpError) {
 | 
			
		||||
		b.Log.Warnf("Couldn't retrieve the display name for %s", mxid)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return b.cacheDisplayName(mxid, mxid[1:])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b.cacheDisplayName(mxid, displayName.DisplayName)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// cacheDisplayName stores the mapping between a mxid and a display name, to be reused later without performing a query to the homserver.
 | 
			
		||||
// Note that old entries are cleaned when this function is called.
 | 
			
		||||
func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
 | 
			
		||||
	// scan to delete old entries, to stop memory usage from becoming too high with old entries.
 | 
			
		||||
	// In addition, we also detect if another user have the same username, and if so, we append their mxids to their usernames to differentiate them.
 | 
			
		||||
	toDelete := []string{}
 | 
			
		||||
	conflict := false
 | 
			
		||||
 | 
			
		||||
	b.Lock()
 | 
			
		||||
	for mxid, v := range b.NicknameMap {
 | 
			
		||||
		// to prevent username reuse across matrix servers - or even on the same server, append
 | 
			
		||||
		// the mxid to the username when there is a conflict
 | 
			
		||||
		if v.displayName == displayName {
 | 
			
		||||
			conflict = true
 | 
			
		||||
			// TODO: it would be nice to be able to rename previous messages from this user.
 | 
			
		||||
			// The current behavior is that only users with clashing usernames and *that have spoken since the bridge last started* will get their mxids shown, and I don't know if that's the expected behavior.
 | 
			
		||||
			v.displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
 | 
			
		||||
			b.NicknameMap[mxid] = v
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if now.Sub(v.lastUpdated) > 10*time.Minute {
 | 
			
		||||
			toDelete = append(toDelete, mxid)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if conflict {
 | 
			
		||||
		displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, v := range toDelete {
 | 
			
		||||
		delete(b.NicknameMap, v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.NicknameMap[mxid] = NicknameCacheEntry{
 | 
			
		||||
		displayName: displayName,
 | 
			
		||||
		lastUpdated: now,
 | 
			
		||||
	}
 | 
			
		||||
	b.Unlock()
 | 
			
		||||
 | 
			
		||||
	return displayName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleError converts errors into httpError.
 | 
			
		||||
//nolint:exhaustivestruct
 | 
			
		||||
func handleError(err error) *httpError {
 | 
			
		||||
	var mErr matrix.HTTPError
 | 
			
		||||
	if !errors.As(err, &mErr) {
 | 
			
		||||
		return &httpError{
 | 
			
		||||
			Err: "not a HTTPError",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var httpErr httpError
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil {
 | 
			
		||||
		return &httpError{
 | 
			
		||||
			Err: "unmarshal failed",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &httpErr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool {
 | 
			
		||||
	// Skip empty messages
 | 
			
		||||
	if content["msgtype"] == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Only allow image,video or file msgtypes
 | 
			
		||||
	if !(content["msgtype"].(string) == "m.image" ||
 | 
			
		||||
		content["msgtype"].(string) == "m.video" ||
 | 
			
		||||
		content["msgtype"].(string) == "m.file") {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getAvatarURL returns the avatar URL of the specified sender.
 | 
			
		||||
func (b *Bmatrix) getAvatarURL(sender string) string {
 | 
			
		||||
	urlPath := b.mc.BuildURL("profile", sender, "avatar_url")
 | 
			
		||||
 | 
			
		||||
	s := struct {
 | 
			
		||||
		AvatarURL string `json:"avatar_url"`
 | 
			
		||||
	}{}
 | 
			
		||||
 | 
			
		||||
	err := b.mc.MakeRequest("GET", urlPath, nil, &s)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("getAvatarURL failed: %s", err)
 | 
			
		||||
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/")
 | 
			
		||||
	if url != "" {
 | 
			
		||||
		url += "?width=37&height=37&method=crop"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return url
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleRatelimit handles the ratelimit errors and return if we're ratelimited and the amount of time to sleep
 | 
			
		||||
func (b *Bmatrix) handleRatelimit(err error) (time.Duration, bool) {
 | 
			
		||||
	httpErr := handleError(err)
 | 
			
		||||
	if httpErr.Errcode != "M_LIMIT_EXCEEDED" {
 | 
			
		||||
		return 0, false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("ratelimited: %s", httpErr.Err)
 | 
			
		||||
	b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before retrying", httpErr.RetryAfterMs/1000)
 | 
			
		||||
 | 
			
		||||
	return time.Duration(httpErr.RetryAfterMs) * time.Millisecond, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// retry function will check if we're ratelimited and retries again when backoff time expired
 | 
			
		||||
// returns original error if not 429 ratelimit
 | 
			
		||||
func (b *Bmatrix) retry(f func() error) error {
 | 
			
		||||
	b.rateMutex.Lock()
 | 
			
		||||
	defer b.rateMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		if err := f(); err != nil {
 | 
			
		||||
			if backoff, ok := b.handleRatelimit(err); ok {
 | 
			
		||||
				time.Sleep(backoff)
 | 
			
		||||
			} else {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -3,11 +3,11 @@ package bmatrix
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
@@ -15,108 +15,40 @@ import (
 | 
			
		||||
	matrix "github.com/matterbridge/gomatrix"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	htmlTag            = regexp.MustCompile("</.*?>")
 | 
			
		||||
	htmlReplacementTag = regexp.MustCompile("<[^>]*>")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type NicknameCacheEntry struct {
 | 
			
		||||
	displayName string
 | 
			
		||||
	lastUpdated time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Bmatrix struct {
 | 
			
		||||
	mc          *matrix.Client
 | 
			
		||||
	UserID      string
 | 
			
		||||
	NicknameMap map[string]NicknameCacheEntry
 | 
			
		||||
	RoomMap     map[string]string
 | 
			
		||||
	rateMutex   sync.RWMutex
 | 
			
		||||
	mc      *matrix.Client
 | 
			
		||||
	UserID  string
 | 
			
		||||
	RoomMap map[string]string
 | 
			
		||||
	sync.RWMutex
 | 
			
		||||
	htmlTag *regexp.Regexp
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type httpError struct {
 | 
			
		||||
	Errcode      string `json:"errcode"`
 | 
			
		||||
	Err          string `json:"error"`
 | 
			
		||||
	RetryAfterMs int    `json:"retry_after_ms"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type matrixUsername struct {
 | 
			
		||||
	plain     string
 | 
			
		||||
	formatted string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubTextMessage represents the new content of the message in edit messages.
 | 
			
		||||
type SubTextMessage struct {
 | 
			
		||||
	MsgType       string `json:"msgtype"`
 | 
			
		||||
	Body          string `json:"body"`
 | 
			
		||||
	FormattedBody string `json:"formatted_body,omitempty"`
 | 
			
		||||
	Format        string `json:"format,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MessageRelation explains how the current message relates to a previous message.
 | 
			
		||||
// Notably used for message edits.
 | 
			
		||||
type MessageRelation struct {
 | 
			
		||||
	EventID string `json:"event_id"`
 | 
			
		||||
	Type    string `json:"rel_type"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EditedMessage struct {
 | 
			
		||||
	NewContent SubTextMessage  `json:"m.new_content"`
 | 
			
		||||
	RelatedTo  MessageRelation `json:"m.relates_to"`
 | 
			
		||||
	matrix.TextMessage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type InReplyToRelationContent struct {
 | 
			
		||||
	EventID string `json:"event_id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type InReplyToRelation struct {
 | 
			
		||||
	InReplyTo InReplyToRelationContent `json:"m.in_reply_to"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ReplyMessage struct {
 | 
			
		||||
	RelatedTo InReplyToRelation `json:"m.relates_to"`
 | 
			
		||||
	matrix.TextMessage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	b := &Bmatrix{Config: cfg}
 | 
			
		||||
	b.htmlTag = regexp.MustCompile("</.*?>")
 | 
			
		||||
	b.RoomMap = make(map[string]string)
 | 
			
		||||
	b.NicknameMap = make(map[string]NicknameCacheEntry)
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) Connect() error {
 | 
			
		||||
	var err error
 | 
			
		||||
	b.Log.Infof("Connecting %s", b.GetString("Server"))
 | 
			
		||||
	if b.GetString("MxID") != "" && b.GetString("Token") != "" {
 | 
			
		||||
		b.mc, err = matrix.NewClient(
 | 
			
		||||
			b.GetString("Server"), b.GetString("MxID"), b.GetString("Token"),
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		b.UserID = b.GetString("MxID")
 | 
			
		||||
		b.Log.Info("Using existing Matrix credentials")
 | 
			
		||||
	} else {
 | 
			
		||||
		b.mc, err = matrix.NewClient(b.GetString("Server"), "", "")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		resp, err := b.mc.Login(&matrix.ReqLogin{
 | 
			
		||||
			Type:       "m.login.password",
 | 
			
		||||
			User:       b.GetString("Login"),
 | 
			
		||||
			Password:   b.GetString("Password"),
 | 
			
		||||
			Identifier: matrix.NewUserIdentifier(b.GetString("Login")),
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		b.mc.SetCredentials(resp.UserID, resp.AccessToken)
 | 
			
		||||
		b.UserID = resp.UserID
 | 
			
		||||
		b.Log.Info("Connection succeeded")
 | 
			
		||||
	b.mc, err = matrix.NewClient(b.GetString("Server"), "", "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	resp, err := b.mc.Login(&matrix.ReqLogin{
 | 
			
		||||
		Type:     "m.login.password",
 | 
			
		||||
		User:     b.GetString("Login"),
 | 
			
		||||
		Password: b.GetString("Password"),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.mc.SetCredentials(resp.UserID, resp.AccessToken)
 | 
			
		||||
	b.UserID = resp.UserID
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	go b.handlematrix()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -126,18 +58,14 @@ func (b *Bmatrix) Disconnect() error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	return b.retry(func() error {
 | 
			
		||||
		resp, err := b.mc.JoinRoom(channel.Name, "", nil)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Lock()
 | 
			
		||||
		b.RoomMap[resp.RoomID] = channel.Name
 | 
			
		||||
		b.Unlock()
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
	resp, err := b.mc.JoinRoom(channel.Name, "", nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.Lock()
 | 
			
		||||
	b.RoomMap[resp.RoomID] = channel.Name
 | 
			
		||||
	b.Unlock()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) Send(msg config.Message) (string, error) {
 | 
			
		||||
@@ -146,61 +74,17 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
 | 
			
		||||
	channel := b.getRoomID(msg.Channel)
 | 
			
		||||
	b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel)
 | 
			
		||||
 | 
			
		||||
	username := newMatrixUsername(msg.Username)
 | 
			
		||||
 | 
			
		||||
	body := username.plain + msg.Text
 | 
			
		||||
	formattedBody := username.formatted + helper.ParseMarkdown(msg.Text)
 | 
			
		||||
 | 
			
		||||
	if b.GetBool("SpoofUsername") {
 | 
			
		||||
		// https://spec.matrix.org/v1.3/client-server-api/#mroommember
 | 
			
		||||
		type stateMember struct {
 | 
			
		||||
			AvatarURL   string `json:"avatar_url,omitempty"`
 | 
			
		||||
			DisplayName string `json:"displayname"`
 | 
			
		||||
			Membership  string `json:"membership"`
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// TODO: reset username afterwards with DisplayName: null ?
 | 
			
		||||
		m := stateMember{
 | 
			
		||||
			AvatarURL:   "",
 | 
			
		||||
			DisplayName: username.plain,
 | 
			
		||||
			Membership:  "join",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err := b.mc.SendStateEvent(channel, "m.room.member", b.UserID, m)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			body = msg.Text
 | 
			
		||||
			formattedBody = helper.ParseMarkdown(msg.Text)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Make a action /me of the message
 | 
			
		||||
	if msg.Event == config.EventUserAction {
 | 
			
		||||
		m := matrix.TextMessage{
 | 
			
		||||
			MsgType:       "m.emote",
 | 
			
		||||
			Body:          body,
 | 
			
		||||
			FormattedBody: formattedBody,
 | 
			
		||||
			Format:        "org.matrix.custom.html",
 | 
			
		||||
			MsgType: "m.emote",
 | 
			
		||||
			Body:    msg.Username + msg.Text,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.GetBool("HTMLDisable") {
 | 
			
		||||
			m.Format = ""
 | 
			
		||||
			m.FormattedBody = ""
 | 
			
		||||
		resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		msgID := ""
 | 
			
		||||
 | 
			
		||||
		err := b.retry(func() error {
 | 
			
		||||
			resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			msgID = resp.EventID
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		return msgID, err
 | 
			
		||||
		return resp.EventID, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Delete message
 | 
			
		||||
@@ -208,34 +92,17 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
 | 
			
		||||
		if msg.ID == "" {
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		msgID := ""
 | 
			
		||||
 | 
			
		||||
		err := b.retry(func() error {
 | 
			
		||||
			resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			msgID = resp.EventID
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		return msgID, err
 | 
			
		||||
		resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return resp.EventID, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Upload a file if it exists
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
			
		||||
			rmsg := rmsg
 | 
			
		||||
 | 
			
		||||
			err := b.retry(func() error {
 | 
			
		||||
				_, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text)
 | 
			
		||||
 | 
			
		||||
				return err
 | 
			
		||||
			})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
			if _, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text); err != nil {
 | 
			
		||||
				b.Log.Errorf("sendText failed: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -246,162 +113,47 @@ 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:          body,
 | 
			
		||||
				MsgType:       "m.text",
 | 
			
		||||
				Format:        "org.matrix.custom.html",
 | 
			
		||||
				FormattedBody: formattedBody,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		rmsg.NewContent = SubTextMessage{
 | 
			
		||||
			Body:          rmsg.TextMessage.Body,
 | 
			
		||||
			FormattedBody: rmsg.TextMessage.FormattedBody,
 | 
			
		||||
			Format:        rmsg.TextMessage.Format,
 | 
			
		||||
			MsgType:       "m.text",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.GetBool("HTMLDisable") {
 | 
			
		||||
			rmsg.TextMessage.Format = ""
 | 
			
		||||
			rmsg.TextMessage.FormattedBody = ""
 | 
			
		||||
			rmsg.NewContent.Format = ""
 | 
			
		||||
			rmsg.NewContent.FormattedBody = ""
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		rmsg.RelatedTo = MessageRelation{
 | 
			
		||||
			EventID: msg.ID,
 | 
			
		||||
			Type:    "m.replace",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := b.retry(func() error {
 | 
			
		||||
			_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return msg.ID, nil
 | 
			
		||||
	}
 | 
			
		||||
	// matrix has no editing support
 | 
			
		||||
 | 
			
		||||
	// Use notices to send join/leave events
 | 
			
		||||
	if msg.Event == config.EventJoinLeave {
 | 
			
		||||
		m := matrix.TextMessage{
 | 
			
		||||
			MsgType:       "m.notice",
 | 
			
		||||
			Body:          body,
 | 
			
		||||
			FormattedBody: formattedBody,
 | 
			
		||||
			Format:        "org.matrix.custom.html",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.GetBool("HTMLDisable") {
 | 
			
		||||
			m.Format = ""
 | 
			
		||||
			m.FormattedBody = ""
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var (
 | 
			
		||||
			resp *matrix.RespSendEvent
 | 
			
		||||
			err  error
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		err = b.retry(func() error {
 | 
			
		||||
			resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
		resp, err := b.mc.SendNotice(channel, msg.Username+msg.Text)
 | 
			
		||||
		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 (
 | 
			
		||||
			resp *matrix.RespSendEvent
 | 
			
		||||
			err  error
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		err = b.retry(func() error {
 | 
			
		||||
			resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return resp.EventID, err
 | 
			
		||||
	username := html.EscapeString(msg.Username)
 | 
			
		||||
	// check if we have a </tag>. if we have, we don't escape HTML. #696
 | 
			
		||||
	if b.htmlTag.MatchString(msg.Username) {
 | 
			
		||||
		username = msg.Username
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.GetBool("HTMLDisable") {
 | 
			
		||||
		var (
 | 
			
		||||
			resp *matrix.RespSendEvent
 | 
			
		||||
			err  error
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		err = b.retry(func() error {
 | 
			
		||||
			resp, err = b.mc.SendText(channel, body)
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return resp.EventID, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post normal message with HTML support (eg riot.im)
 | 
			
		||||
	var (
 | 
			
		||||
		resp *matrix.RespSendEvent
 | 
			
		||||
		err  error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	err = b.retry(func() error {
 | 
			
		||||
		resp, err = b.mc.SendFormattedText(channel, body, formattedBody)
 | 
			
		||||
 | 
			
		||||
		return err
 | 
			
		||||
	})
 | 
			
		||||
	resp, err := b.mc.SendHTML(channel, msg.Username+msg.Text, username+helper.ParseMarkdown(msg.Text))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resp.EventID, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) getRoomID(channel string) string {
 | 
			
		||||
	b.RLock()
 | 
			
		||||
	defer b.RUnlock()
 | 
			
		||||
	for ID, name := range b.RoomMap {
 | 
			
		||||
		if name == channel {
 | 
			
		||||
			return ID
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) handlematrix() {
 | 
			
		||||
	syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
 | 
			
		||||
	syncer.OnEventType("m.room.redaction", b.handleEvent)
 | 
			
		||||
	syncer.OnEventType("m.room.message", b.handleEvent)
 | 
			
		||||
	syncer.OnEventType("m.room.member", b.handleMemberChange)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			if b == nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if err := b.mc.Sync(); err != nil {
 | 
			
		||||
				b.Log.Println("Sync() returned ", err)
 | 
			
		||||
			}
 | 
			
		||||
@@ -409,77 +161,6 @@ func (b *Bmatrix) handlematrix() {
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool {
 | 
			
		||||
	relationInterface, present := ev.Content["m.relates_to"]
 | 
			
		||||
	newContentInterface, present2 := ev.Content["m.new_content"]
 | 
			
		||||
	if !(present && present2) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var relation MessageRelation
 | 
			
		||||
	if err := interface2Struct(relationInterface, &relation); err != nil {
 | 
			
		||||
		b.Log.Warnf("Couldn't parse 'm.relates_to' object with value %#v", relationInterface)
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var newContent SubTextMessage
 | 
			
		||||
	if err := interface2Struct(newContentInterface, &newContent); err != nil {
 | 
			
		||||
		b.Log.Warnf("Couldn't parse 'm.new_content' object with value %#v", newContentInterface)
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if relation.Type != "m.replace" {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg.ID = relation.EventID
 | 
			
		||||
	rmsg.Text = newContent.Body
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool {
 | 
			
		||||
	relationInterface, present := ev.Content["m.relates_to"]
 | 
			
		||||
	if !present {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var relation InReplyToRelation
 | 
			
		||||
	if err := interface2Struct(relationInterface, &relation); err != nil {
 | 
			
		||||
		// probably fine
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body := rmsg.Text
 | 
			
		||||
 | 
			
		||||
	if !b.GetBool("keepquotedreply") {
 | 
			
		||||
		for strings.HasPrefix(body, "> ") {
 | 
			
		||||
			lineIdx := strings.IndexRune(body, '\n')
 | 
			
		||||
			if lineIdx == -1 {
 | 
			
		||||
				body = ""
 | 
			
		||||
			} else {
 | 
			
		||||
				body = body[(lineIdx + 1):]
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg.Text = body
 | 
			
		||||
	rmsg.ParentID = relation.InReplyTo.EventID
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) handleMemberChange(ev *matrix.Event) {
 | 
			
		||||
	// Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information
 | 
			
		||||
	if ev.Content["membership"] == "join" {
 | 
			
		||||
		if dn, ok := ev.Content["displayname"].(string); ok {
 | 
			
		||||
			b.cacheDisplayName(ev.Sender, dn)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmatrix) handleEvent(ev *matrix.Event) {
 | 
			
		||||
	b.Log.Debugf("== Receiving event: %#v", ev)
 | 
			
		||||
	if ev.Sender != b.UserID {
 | 
			
		||||
@@ -491,14 +172,16 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// TODO download avatar
 | 
			
		||||
 | 
			
		||||
		// Create our message
 | 
			
		||||
		rmsg := config.Message{
 | 
			
		||||
			Username: b.getDisplayName(ev.Sender),
 | 
			
		||||
			Channel:  channel,
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			UserID:   ev.Sender,
 | 
			
		||||
			ID:       ev.ID,
 | 
			
		||||
			Avatar:   b.getAvatarURL(ev.Sender),
 | 
			
		||||
		rmsg := config.Message{Username: ev.Sender[1:], Channel: channel, Account: b.Account, UserID: ev.Sender, ID: ev.ID}
 | 
			
		||||
 | 
			
		||||
		// Text must be a string
 | 
			
		||||
		if rmsg.Text, ok = ev.Content["body"].(string); !ok {
 | 
			
		||||
			b.Log.Errorf("Content[body] is not a string: %T\n%#v",
 | 
			
		||||
				ev.Content["body"], ev.Content)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Remove homeserver suffix if configured
 | 
			
		||||
@@ -516,28 +199,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Text must be a string
 | 
			
		||||
		if rmsg.Text, ok = ev.Content["body"].(string); !ok {
 | 
			
		||||
			b.Log.Errorf("Content[body] is not a string: %T\n%#v",
 | 
			
		||||
				ev.Content["body"], ev.Content)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Do we have a /me action
 | 
			
		||||
		if ev.Content["msgtype"].(string) == "m.emote" {
 | 
			
		||||
			rmsg.Event = config.EventUserAction
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Is it an edit?
 | 
			
		||||
		if b.handleEdit(ev, rmsg) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Is it a reply?
 | 
			
		||||
		if b.handleReply(ev, rmsg) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Do we have attachments
 | 
			
		||||
		if b.containsAttachment(ev.Content) {
 | 
			
		||||
			err := b.handleDownloadFile(&rmsg, ev.Content)
 | 
			
		||||
@@ -548,11 +214,6 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
 | 
			
		||||
 | 
			
		||||
		b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
 | 
			
		||||
		b.Remote <- rmsg
 | 
			
		||||
 | 
			
		||||
		// not crucial, so no ratelimit check here
 | 
			
		||||
		if err := b.mc.MarkRead(ev.RoomID, ev.ID); err != nil {
 | 
			
		||||
			b.Log.Errorf("couldn't mark message as read %s", err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -627,30 +288,26 @@ func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string
 | 
			
		||||
 | 
			
		||||
// handleUploadFile handles native upload of a file.
 | 
			
		||||
func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) {
 | 
			
		||||
	username := newMatrixUsername(msg.Username)
 | 
			
		||||
	content := bytes.NewReader(*fi.Data)
 | 
			
		||||
	sp := strings.Split(fi.Name, ".")
 | 
			
		||||
	mtype := mime.TypeByExtension("." + sp[len(sp)-1])
 | 
			
		||||
	// image and video uploads send no username, we have to do this ourself here #715
 | 
			
		||||
	err := b.retry(func() error {
 | 
			
		||||
		_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment)
 | 
			
		||||
 | 
			
		||||
		return err
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("file comment failed: %#v", err)
 | 
			
		||||
	if !strings.Contains(mtype, "image") && !strings.Contains(mtype, "video") {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if fi.Comment != "" {
 | 
			
		||||
		_, err := b.mc.SendText(channel, msg.Username+fi.Comment)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("file comment failed: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		// image and video uploads send no username, we have to do this ourself here #715
 | 
			
		||||
		_, err := b.mc.SendText(channel, msg.Username)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("file comment failed: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
 | 
			
		||||
 | 
			
		||||
	var res *matrix.RespMediaUpload
 | 
			
		||||
 | 
			
		||||
	err = b.retry(func() error {
 | 
			
		||||
		res, err = b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
 | 
			
		||||
 | 
			
		||||
		return err
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("file upload failed: %#v", err)
 | 
			
		||||
		return
 | 
			
		||||
@@ -659,60 +316,32 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
 | 
			
		||||
	switch {
 | 
			
		||||
	case strings.Contains(mtype, "video"):
 | 
			
		||||
		b.Log.Debugf("sendVideo %s", res.ContentURI)
 | 
			
		||||
		err = b.retry(func() error {
 | 
			
		||||
			_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
		_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("sendVideo failed: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
	case strings.Contains(mtype, "image"):
 | 
			
		||||
		b.Log.Debugf("sendImage %s", res.ContentURI)
 | 
			
		||||
		err = b.retry(func() error {
 | 
			
		||||
			_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
		_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("sendImage failed: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
	case strings.Contains(mtype, "audio"):
 | 
			
		||||
		b.Log.Debugf("sendAudio %s", res.ContentURI)
 | 
			
		||||
		err = b.retry(func() error {
 | 
			
		||||
			_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{
 | 
			
		||||
				MsgType: "m.audio",
 | 
			
		||||
				Body:    fi.Name,
 | 
			
		||||
				URL:     res.ContentURI,
 | 
			
		||||
				Info: matrix.AudioInfo{
 | 
			
		||||
					Mimetype: mtype,
 | 
			
		||||
					Size:     uint(len(*fi.Data)),
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("sendAudio failed: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		b.Log.Debugf("sendFile %s", res.ContentURI)
 | 
			
		||||
		err = b.retry(func() error {
 | 
			
		||||
			_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{
 | 
			
		||||
				MsgType: "m.file",
 | 
			
		||||
				Body:    fi.Name,
 | 
			
		||||
				URL:     res.ContentURI,
 | 
			
		||||
				Info: matrix.FileInfo{
 | 
			
		||||
					Mimetype: mtype,
 | 
			
		||||
					Size:     uint(len(*fi.Data)),
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			return err
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("sendFile failed: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("result: %#v", res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// skipMessages returns true if this message should not be handled
 | 
			
		||||
func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool {
 | 
			
		||||
	// Skip empty messages
 | 
			
		||||
	if content["msgtype"] == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Only allow image,video or file msgtypes
 | 
			
		||||
	if !(content["msgtype"].(string) == "m.image" ||
 | 
			
		||||
		content["msgtype"].(string) == "m.video" ||
 | 
			
		||||
		content["msgtype"].(string) == "m.file") {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
package bmatrix
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestPlainUsername(t *testing.T) {
 | 
			
		||||
	uut := newMatrixUsername("MyUser")
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, "MyUser", uut.formatted)
 | 
			
		||||
	assert.Equal(t, "MyUser", uut.plain)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestHTMLUsername(t *testing.T) {
 | 
			
		||||
	uut := newMatrixUsername("<b>MyUser</b>")
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, "<b>MyUser</b>", uut.formatted)
 | 
			
		||||
	assert.Equal(t, "MyUser", uut.plain)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestFancyUsername(t *testing.T) {
 | 
			
		||||
	uut := newMatrixUsername("<MyUser>")
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, "<MyUser>", uut.formatted)
 | 
			
		||||
	assert.Equal(t, "<MyUser>", uut.plain)
 | 
			
		||||
}
 | 
			
		||||
@@ -3,8 +3,8 @@ package bmattermost
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/matterbridge/matterclient"
 | 
			
		||||
	"github.com/mattermost/mattermost-server/v6/model"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterclient"
 | 
			
		||||
	"github.com/mattermost/mattermost-server/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// handleDownloadAvatar downloads the avatar of userid from channel
 | 
			
		||||
@@ -21,17 +21,12 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
	}
 | 
			
		||||
	if _, ok := b.avatarMap[userid]; !ok {
 | 
			
		||||
		var (
 | 
			
		||||
			data []byte
 | 
			
		||||
			err  error
 | 
			
		||||
		)
 | 
			
		||||
		data, _, err = b.mc.Client.GetProfileImage(userid, "")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err)
 | 
			
		||||
		data, resp := b.mc.Client.GetProfileImage(userid, "")
 | 
			
		||||
		if resp.Error != nil {
 | 
			
		||||
			b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
 | 
			
		||||
		err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Error(err)
 | 
			
		||||
			return
 | 
			
		||||
@@ -41,20 +36,20 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//nolint:wrapcheck
 | 
			
		||||
// handleDownloadFile handles file download
 | 
			
		||||
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
 | 
			
		||||
	url, _, _ := b.mc.Client.GetFileLink(id)
 | 
			
		||||
	finfo, _, err := b.mc.Client.GetFileInfo(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
 | 
			
		||||
	}
 | 
			
		||||
	err = helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	data, _, err := b.mc.Client.DownloadFile(id, true)
 | 
			
		||||
	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
 | 
			
		||||
@@ -71,10 +66,6 @@ func (b *Bmattermost) handleMatter() {
 | 
			
		||||
		} else {
 | 
			
		||||
			b.Log.Debugf("Choosing login/password based receiving")
 | 
			
		||||
		}
 | 
			
		||||
		// if for some reason we only want to sent stuff to mattermost but not receive, return
 | 
			
		||||
		if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") != "" && b.GetString("Token") == "" && b.GetString("Login") == "" {
 | 
			
		||||
			b.Log.Debugf("No WebhookBindAddress specified, only WebhookURL. You will not receive messages from mattermost, only sending is possible.")
 | 
			
		||||
		}
 | 
			
		||||
		go b.handleMatterClient(messages)
 | 
			
		||||
	}
 | 
			
		||||
	var ok bool
 | 
			
		||||
@@ -91,24 +82,18 @@ func (b *Bmattermost) handleMatter() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//nolint:cyclop
 | 
			
		||||
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
 | 
			
		||||
	for message := range b.mc.MessageChan {
 | 
			
		||||
		b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType())
 | 
			
		||||
		b.Log.Debugf("%#v", message.Raw.Data)
 | 
			
		||||
 | 
			
		||||
		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, channelName)
 | 
			
		||||
			b.handleDownloadAvatar(message.UserID, message.Channel)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Log.Debugf("== Receiving event %#v", message)
 | 
			
		||||
@@ -116,10 +101,10 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
 | 
			
		||||
		rmsg := &config.Message{
 | 
			
		||||
			Username: message.Username,
 | 
			
		||||
			UserID:   message.UserID,
 | 
			
		||||
			Channel:  channelName,
 | 
			
		||||
			Channel:  message.Channel,
 | 
			
		||||
			Text:     message.Text,
 | 
			
		||||
			ID:       message.Post.Id,
 | 
			
		||||
			ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
 | 
			
		||||
			ParentID: message.Post.ParentId,
 | 
			
		||||
			Extra:    make(map[string][]interface{}),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -127,11 +112,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.EventType() == model.WebsocketEventPostEdited && !b.GetBool("EditDisable") {
 | 
			
		||||
		if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") {
 | 
			
		||||
			rmsg.Text = message.Text + b.GetString("EditSuffix")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if message.Raw.EventType() == model.WebsocketEventPostDeleted {
 | 
			
		||||
		if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED {
 | 
			
		||||
			rmsg.Event = config.EventMsgDelete
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -143,10 +128,8 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Use nickname instead of username if defined
 | 
			
		||||
		if !b.GetBool("useusername") {
 | 
			
		||||
			if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
 | 
			
		||||
				rmsg.Username = nick
 | 
			
		||||
			}
 | 
			
		||||
		if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
 | 
			
		||||
			rmsg.Username = nick
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		messages <- rmsg
 | 
			
		||||
@@ -157,7 +140,6 @@ 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,
 | 
			
		||||
@@ -167,10 +149,11 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleUploadFile handles native upload of files
 | 
			
		||||
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
	var res, id string
 | 
			
		||||
	channelID := b.getChannelID(msg.Channel)
 | 
			
		||||
	channelID := b.mc.GetChannelId(msg.Channel, b.TeamID)
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
		id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
 | 
			
		||||
@@ -186,7 +169,6 @@ func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
 | 
			
		||||
	return res, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//nolint:forcetypeassert
 | 
			
		||||
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
 | 
			
		||||
	props := message.Post.Props
 | 
			
		||||
	if props == nil {
 | 
			
		||||
@@ -197,18 +179,16 @@ func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Me
 | 
			
		||||
	}
 | 
			
		||||
	if _, ok := props["attachments"].([]interface{}); ok {
 | 
			
		||||
		rmsg.Extra["attachments"] = props["attachments"].([]interface{})
 | 
			
		||||
		if rmsg.Text != "" {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, attachment := range rmsg.Extra["attachments"] {
 | 
			
		||||
			attach := attachment.(map[string]interface{})
 | 
			
		||||
			if attach["text"].(string) != "" {
 | 
			
		||||
				rmsg.Text += attach["text"].(string)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if attach["fallback"].(string) != "" {
 | 
			
		||||
				rmsg.Text += attach["fallback"].(string)
 | 
			
		||||
		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)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,13 @@
 | 
			
		||||
package bmattermost
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterclient"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterhook"
 | 
			
		||||
	"github.com/matterbridge/matterclient"
 | 
			
		||||
	"github.com/mattermost/mattermost-server/v6/model"
 | 
			
		||||
	"github.com/mattermost/mattermost-server/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Bmattermost) doConnectWebhookBind() error {
 | 
			
		||||
@@ -16,10 +15,8 @@ func (b *Bmattermost) doConnectWebhookBind() error {
 | 
			
		||||
	case b.GetString("WebhookURL") != "":
 | 
			
		||||
		b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
 | 
			
		||||
		b.mh = matterhook.New(b.GetString("WebhookURL"),
 | 
			
		||||
			matterhook.Config{
 | 
			
		||||
				InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
			
		||||
				BindAddress:        b.GetString("WebhookBindAddress"),
 | 
			
		||||
			})
 | 
			
		||||
			matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
			
		||||
				BindAddress: b.GetString("WebhookBindAddress")})
 | 
			
		||||
	case b.GetString("Token") != "":
 | 
			
		||||
		b.Log.Info("Connecting using token (sending)")
 | 
			
		||||
		err := b.apiLogin()
 | 
			
		||||
@@ -35,10 +32,8 @@ func (b *Bmattermost) doConnectWebhookBind() error {
 | 
			
		||||
	default:
 | 
			
		||||
		b.Log.Info("Connecting using webhookbindaddress (receiving)")
 | 
			
		||||
		b.mh = matterhook.New(b.GetString("WebhookURL"),
 | 
			
		||||
			matterhook.Config{
 | 
			
		||||
				InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
			
		||||
				BindAddress:        b.GetString("WebhookBindAddress"),
 | 
			
		||||
			})
 | 
			
		||||
			matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
			
		||||
				BindAddress: b.GetString("WebhookBindAddress")})
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -46,10 +41,8 @@ func (b *Bmattermost) doConnectWebhookBind() error {
 | 
			
		||||
func (b *Bmattermost) doConnectWebhookURL() error {
 | 
			
		||||
	b.Log.Info("Connecting using webhookurl (sending)")
 | 
			
		||||
	b.mh = matterhook.New(b.GetString("WebhookURL"),
 | 
			
		||||
		matterhook.Config{
 | 
			
		||||
			InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
			
		||||
			DisableServer:      true,
 | 
			
		||||
		})
 | 
			
		||||
		matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
 | 
			
		||||
			DisableServer: true})
 | 
			
		||||
	if b.GetString("Token") != "" {
 | 
			
		||||
		b.Log.Info("Connecting using token (receiving)")
 | 
			
		||||
		err := b.apiLogin()
 | 
			
		||||
@@ -66,28 +59,27 @@ func (b *Bmattermost) doConnectWebhookURL() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//nolint:wrapcheck
 | 
			
		||||
func (b *Bmattermost) apiLogin() error {
 | 
			
		||||
	password := b.GetString("Password")
 | 
			
		||||
	if b.GetString("Token") != "" {
 | 
			
		||||
		password = "token=" + b.GetString("Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"), "")
 | 
			
		||||
	b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"))
 | 
			
		||||
	if b.GetBool("debug") {
 | 
			
		||||
		b.mc.SetLogLevel("debug")
 | 
			
		||||
	}
 | 
			
		||||
	b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify")
 | 
			
		||||
	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"))
 | 
			
		||||
 | 
			
		||||
	if err := b.mc.Login(); err != nil {
 | 
			
		||||
	err := b.mc.Login()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	b.TeamID = b.mc.GetTeamID()
 | 
			
		||||
	b.TeamID = b.mc.GetTeamId()
 | 
			
		||||
	go b.mc.WsReceiver()
 | 
			
		||||
	go b.mc.StatusLoop()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -120,7 +112,6 @@ 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) {
 | 
			
		||||
@@ -144,7 +135,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
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -171,7 +162,6 @@ 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" ||
 | 
			
		||||
@@ -180,17 +170,11 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
 | 
			
		||||
		if b.GetBool("nosendjoinpart") {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		channelName := b.getChannelName(message.Post.ChannelId)
 | 
			
		||||
		if channelName == "" {
 | 
			
		||||
			channelName = message.Channel
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
 | 
			
		||||
		b.Remote <- config.Message{
 | 
			
		||||
			Username: "system",
 | 
			
		||||
			Text:     message.Text,
 | 
			
		||||
			Channel:  channelName,
 | 
			
		||||
			Channel:  message.Channel,
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			Event:    config.EventJoinLeave,
 | 
			
		||||
		}
 | 
			
		||||
@@ -198,7 +182,7 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle edited messages
 | 
			
		||||
	if (message.Raw.EventType() == model.WebsocketEventPostEdited) && b.GetBool("EditDisable") {
 | 
			
		||||
	if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -211,14 +195,13 @@ 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.Debug("sent by matterbridge, ignoring")
 | 
			
		||||
			b.Log.Debugf("sent by matterbridge, ignoring")
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ignore messages sent from a user logged in as the bot
 | 
			
		||||
	if b.mc.User.Username == message.Username {
 | 
			
		||||
		b.Log.Debug("message from same user as bot, ignoring")
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -228,56 +211,14 @@ func (b *Bmattermost) skipMessage(message *matterclient.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")
 | 
			
		||||
	if message.Raw.Data["team_id"].(string) != b.TeamID {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// only handle posted, edited or deleted events
 | 
			
		||||
	if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model.WebsocketEventPostEdited ||
 | 
			
		||||
		message.Raw.EventType() == model.WebsocketEventPostDeleted) {
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmattermost) getVersion() string {
 | 
			
		||||
	proto := "https"
 | 
			
		||||
 | 
			
		||||
	if b.GetBool("notls") {
 | 
			
		||||
		proto = "http"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.Get(proto + "://" + b.GetString("server"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Error("failed getting version")
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	return resp.Header.Get("X-Version-Id")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmattermost) getChannelID(name string) string {
 | 
			
		||||
	idcheck := strings.Split(name, "ID:")
 | 
			
		||||
	if len(idcheck) > 1 {
 | 
			
		||||
		return idcheck[1]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b.mc.GetChannelID(name, b.TeamID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmattermost) getChannelName(id string) string {
 | 
			
		||||
	b.channelsMutex.RLock()
 | 
			
		||||
	defer b.channelsMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	for _, c := range b.channelInfoMap {
 | 
			
		||||
		if c.Name == "ID:"+id {
 | 
			
		||||
			// if we have ID: specified in our gateway configuration return this
 | 
			
		||||
			return c.Name
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,41 +3,29 @@ package bmattermost
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterclient"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterhook"
 | 
			
		||||
	"github.com/matterbridge/matterclient"
 | 
			
		||||
	"github.com/rs/xid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Bmattermost struct {
 | 
			
		||||
	mh     *matterhook.Client
 | 
			
		||||
	mc     *matterclient.Client
 | 
			
		||||
	v6     bool
 | 
			
		||||
	mc     *matterclient.MMClient
 | 
			
		||||
	uuid   string
 | 
			
		||||
	TeamID string
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
	avatarMap      map[string]string
 | 
			
		||||
	channelsMutex  sync.RWMutex
 | 
			
		||||
	channelInfoMap map[string]*config.ChannelInfo
 | 
			
		||||
	avatarMap map[string]string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mattermostPlugin = "mattermost.plugin"
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	b := &Bmattermost{
 | 
			
		||||
		Config:         cfg,
 | 
			
		||||
		avatarMap:      make(map[string]string),
 | 
			
		||||
		channelInfoMap: make(map[string]*config.ChannelInfo),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.v6 = b.GetBool("v6")
 | 
			
		||||
	b := &Bmattermost{Config: cfg, avatarMap: make(map[string]string)}
 | 
			
		||||
	b.uuid = xid.New().String()
 | 
			
		||||
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -49,13 +37,6 @@ func (b *Bmattermost) Connect() error {
 | 
			
		||||
	if b.Account == mattermostPlugin {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.HasPrefix(b.getVersion(), "6.") || strings.HasPrefix(b.getVersion(), "7.") {
 | 
			
		||||
		if !b.v6 {
 | 
			
		||||
			b.v6 = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.GetString("WebhookBindAddress") != "" {
 | 
			
		||||
		if err := b.doConnectWebhookBind(); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
@@ -79,7 +60,6 @@ func (b *Bmattermost) Connect() error {
 | 
			
		||||
		go b.handleMatter()
 | 
			
		||||
	case b.GetString("Login") != "":
 | 
			
		||||
		b.Log.Info("Connecting using login/password (sending and receiving)")
 | 
			
		||||
		b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
 | 
			
		||||
		err := b.apiLogin()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
@@ -101,21 +81,14 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	if b.Account == mattermostPlugin {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.channelsMutex.Lock()
 | 
			
		||||
	b.channelInfoMap[channel.ID] = &channel
 | 
			
		||||
	b.channelsMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	// we can only join channels using the API
 | 
			
		||||
	if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" {
 | 
			
		||||
		id := b.getChannelID(channel.Name)
 | 
			
		||||
		id := b.mc.GetChannelId(channel.Name, b.TeamID)
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return b.mc.JoinChannel(id)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -145,31 +118,19 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
 | 
			
		||||
		if msg.ID == "" {
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return msg.ID, b.mc.DeleteMessage(msg.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle prefix hint for unthreaded messages.
 | 
			
		||||
	if msg.ParentNotFound() {
 | 
			
		||||
	if msg.ParentID == "msg-parent-not-found" {
 | 
			
		||||
		msg.ParentID = ""
 | 
			
		||||
		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// we only can reply to the root of the thread, not to a specific ID (like discord for example does)
 | 
			
		||||
	if msg.ParentID != "" {
 | 
			
		||||
		post, _, err := b.mc.Client.GetPost(msg.ParentID, "")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
 | 
			
		||||
		}
 | 
			
		||||
		if post.RootId != "" {
 | 
			
		||||
			msg.ParentID = post.RootId
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Upload a file if it exists
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
			
		||||
			if _, err := b.mc.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
 | 
			
		||||
			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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -189,5 +150,5 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post normal message
 | 
			
		||||
	return b.mc.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID)
 | 
			
		||||
	return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,101 +0,0 @@
 | 
			
		||||
package bmsteams
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
 | 
			
		||||
	msgraph "github.com/yaegashi/msgraph.go/beta"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) findFile(weburl string) (string, error) {
 | 
			
		||||
	itemRB, err := b.gc.GetDriveItemByURL(b.ctx, weburl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	itemRB.Workbook().Worksheets()
 | 
			
		||||
	b.gc.Workbooks()
 | 
			
		||||
	item, err := itemRB.Request().Get(b.ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	if url, ok := item.GetAdditionalData("@microsoft.graph.downloadUrl"); ok {
 | 
			
		||||
		return url.(string), nil
 | 
			
		||||
	}
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleDownloadFile handles file download
 | 
			
		||||
func (b *Bmsteams) handleDownloadFile(rmsg *config.Message, filename, weburl string) error {
 | 
			
		||||
	realURL, err := b.findFile(weburl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	// Actually download the file.
 | 
			
		||||
	data, err := helper.DownloadFile(realURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("download %s failed %#v", weburl, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If a comment is attached to the file(s) it is in the 'Text' field of the teams messge event
 | 
			
		||||
	// and should be added as comment to only one of the files. We reset the 'Text' field to ensure
 | 
			
		||||
	// that the comment is not duplicated.
 | 
			
		||||
	comment := rmsg.Text
 | 
			
		||||
	rmsg.Text = ""
 | 
			
		||||
	helper.HandleDownloadData(b.Log, rmsg, filename, comment, weburl, data, b.General)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) handleAttachments(rmsg *config.Message, msg msgraph.ChatMessage) {
 | 
			
		||||
	for _, a := range msg.Attachments {
 | 
			
		||||
		//remove the attachment tags from the text
 | 
			
		||||
		rmsg.Text = attachRE.ReplaceAllString(rmsg.Text, "")
 | 
			
		||||
 | 
			
		||||
		//handle a code snippet (code block)
 | 
			
		||||
		if *a.ContentType == "application/vnd.microsoft.card.codesnippet" {
 | 
			
		||||
			b.handleCodeSnippet(rmsg, a)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//handle the download
 | 
			
		||||
		err := b.handleDownloadFile(rmsg, *a.Name, *a.ContentURL)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("download of %s failed: %s", *a.Name, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AttachContent struct {
 | 
			
		||||
	Language       string `json:"language"`
 | 
			
		||||
	CodeSnippetURL string `json:"codeSnippetUrl"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) handleCodeSnippet(rmsg *config.Message, attach msgraph.ChatMessageAttachment) {
 | 
			
		||||
	var content AttachContent
 | 
			
		||||
	err := json.Unmarshal([]byte(*attach.Content), &content)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("unmarshal codesnippet failed: %s", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	s := strings.Split(content.CodeSnippetURL, "/")
 | 
			
		||||
	if len(s) != 13 {
 | 
			
		||||
		b.Log.Errorf("codesnippetUrl has unexpected size: %s", content.CodeSnippetURL)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp, err := b.gc.Teams().Request().Client().Get(content.CodeSnippetURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("retrieving snippet content failed:%s", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	res, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("reading snippet data failed: %s", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	rmsg.Text = rmsg.Text + "\n```" + content.Language + "\n" + string(res) + "\n```\n"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,229 +0,0 @@
 | 
			
		||||
package bmsteams
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/davecgh/go-spew/spew"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattn/godown"
 | 
			
		||||
	msgraph "github.com/yaegashi/msgraph.go/beta"
 | 
			
		||||
	"github.com/yaegashi/msgraph.go/msauth"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/oauth2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"}
 | 
			
		||||
	attachRE      = regexp.MustCompile(`<attachment id=.*?attachment>`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Bmsteams struct {
 | 
			
		||||
	gc    *msgraph.GraphServiceRequestBuilder
 | 
			
		||||
	ctx   context.Context
 | 
			
		||||
	botID string
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	return &Bmsteams{Config: cfg}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) Connect() error {
 | 
			
		||||
	tokenCachePath := b.GetString("sessionFile")
 | 
			
		||||
	if tokenCachePath == "" {
 | 
			
		||||
		tokenCachePath = "msteams_session.json"
 | 
			
		||||
	}
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	m := msauth.NewManager()
 | 
			
		||||
	m.LoadFile(tokenCachePath) //nolint:errcheck
 | 
			
		||||
	ts, err := m.DeviceAuthorizationGrant(ctx, b.GetString("TenantID"), b.GetString("ClientID"), defaultScopes, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = m.SaveFile(tokenCachePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err)
 | 
			
		||||
	}
 | 
			
		||||
	// make file readable only for matterbridge user
 | 
			
		||||
	err = os.Chmod(tokenCachePath, 0o600)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err)
 | 
			
		||||
	}
 | 
			
		||||
	httpClient := oauth2.NewClient(ctx, ts)
 | 
			
		||||
	graphClient := msgraph.NewClient(httpClient)
 | 
			
		||||
	b.gc = graphClient
 | 
			
		||||
	b.ctx = ctx
 | 
			
		||||
 | 
			
		||||
	err = b.setBotID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) Disconnect() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	go func(name string) {
 | 
			
		||||
		for {
 | 
			
		||||
			err := b.poll(name)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Errorf("polling failed for %s: %s. retrying in 5 seconds", name, err)
 | 
			
		||||
			}
 | 
			
		||||
			time.Sleep(time.Second * 5)
 | 
			
		||||
		}
 | 
			
		||||
	}(channel.Name)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) Send(msg config.Message) (string, error) {
 | 
			
		||||
	b.Log.Debugf("=> Receiving %#v", msg)
 | 
			
		||||
	if msg.ParentValid() {
 | 
			
		||||
		return b.sendReply(msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle prefix hint for unthreaded messages.
 | 
			
		||||
	if msg.ParentNotFound() {
 | 
			
		||||
		msg.ParentID = ""
 | 
			
		||||
		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request()
 | 
			
		||||
	text := msg.Username + msg.Text
 | 
			
		||||
	content := &msgraph.ItemBody{Content: &text}
 | 
			
		||||
	rmsg := &msgraph.ChatMessage{Body: content}
 | 
			
		||||
	res, err := ct.Add(b.ctx, rmsg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return *res.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) sendReply(msg config.Message) (string, error) {
 | 
			
		||||
	ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().ID(msg.ParentID).Replies().Request()
 | 
			
		||||
	// Handle prefix hint for unthreaded messages.
 | 
			
		||||
 | 
			
		||||
	text := msg.Username + msg.Text
 | 
			
		||||
	content := &msgraph.ItemBody{Content: &text}
 | 
			
		||||
	rmsg := &msgraph.ChatMessage{Body: content}
 | 
			
		||||
	res, err := ct.Add(b.ctx, rmsg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return *res.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) getMessages(channel string) ([]msgraph.ChatMessage, error) {
 | 
			
		||||
	ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(channel).Messages().Request()
 | 
			
		||||
	rct, err := ct.Get(b.ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("got %#v messages", len(rct))
 | 
			
		||||
	return rct, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//nolint:gocognit
 | 
			
		||||
func (b *Bmsteams) poll(channelName string) error {
 | 
			
		||||
	msgmap := make(map[string]time.Time)
 | 
			
		||||
	b.Log.Debug("getting initial messages")
 | 
			
		||||
	res, err := b.getMessages(channelName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for _, msg := range res {
 | 
			
		||||
		msgmap[*msg.ID] = *msg.CreatedDateTime
 | 
			
		||||
		if msg.LastModifiedDateTime != nil {
 | 
			
		||||
			msgmap[*msg.ID] = *msg.LastModifiedDateTime
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	time.Sleep(time.Second * 5)
 | 
			
		||||
	b.Log.Debug("polling for messages")
 | 
			
		||||
	for {
 | 
			
		||||
		res, err := b.getMessages(channelName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		for i := len(res) - 1; i >= 0; i-- {
 | 
			
		||||
			msg := res[i]
 | 
			
		||||
			if mtime, ok := msgmap[*msg.ID]; ok {
 | 
			
		||||
				if mtime == *msg.CreatedDateTime && msg.LastModifiedDateTime == nil {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				if msg.LastModifiedDateTime != nil && mtime == *msg.LastModifiedDateTime {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if b.GetBool("debug") {
 | 
			
		||||
				b.Log.Debug("Msg dump: ", spew.Sdump(msg))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// skip non-user message for now.
 | 
			
		||||
			if msg.From == nil || msg.From.User == nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if *msg.From.User.ID == b.botID {
 | 
			
		||||
				b.Log.Debug("skipping own message")
 | 
			
		||||
				msgmap[*msg.ID] = *msg.CreatedDateTime
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			msgmap[*msg.ID] = *msg.CreatedDateTime
 | 
			
		||||
			if msg.LastModifiedDateTime != nil {
 | 
			
		||||
				msgmap[*msg.ID] = *msg.LastModifiedDateTime
 | 
			
		||||
			}
 | 
			
		||||
			b.Log.Debugf("<= Sending message from %s on %s to gateway", *msg.From.User.DisplayName, b.Account)
 | 
			
		||||
			text := b.convertToMD(*msg.Body.Content)
 | 
			
		||||
			rmsg := config.Message{
 | 
			
		||||
				Username: *msg.From.User.DisplayName,
 | 
			
		||||
				Text:     text,
 | 
			
		||||
				Channel:  channelName,
 | 
			
		||||
				Account:  b.Account,
 | 
			
		||||
				Avatar:   "",
 | 
			
		||||
				UserID:   *msg.From.User.ID,
 | 
			
		||||
				ID:       *msg.ID,
 | 
			
		||||
				Extra:    make(map[string][]interface{}),
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			b.handleAttachments(&rmsg, msg)
 | 
			
		||||
			b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
			b.Remote <- rmsg
 | 
			
		||||
		}
 | 
			
		||||
		time.Sleep(time.Second * 5)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) setBotID() error {
 | 
			
		||||
	req := b.gc.Me().Request()
 | 
			
		||||
	r, err := req.Get(b.ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.botID = *r.ID
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmsteams) convertToMD(text string) string {
 | 
			
		||||
	if !strings.Contains(text, "<div>") {
 | 
			
		||||
		return text
 | 
			
		||||
	}
 | 
			
		||||
	var sb strings.Builder
 | 
			
		||||
	err := godown.Convert(&sb, strings.NewReader(text), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Couldn't convert message to markdown %s", text)
 | 
			
		||||
		return text
 | 
			
		||||
	}
 | 
			
		||||
	return sb.String()
 | 
			
		||||
}
 | 
			
		||||
@@ -1,70 +0,0 @@
 | 
			
		||||
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() {
 | 
			
		||||
}
 | 
			
		||||
@@ -1,152 +0,0 @@
 | 
			
		||||
package bmumble
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"layeh.com/gumble/gumble"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) {
 | 
			
		||||
	b.serverConfigUpdate <- *event
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
 | 
			
		||||
	sender := "unknown"
 | 
			
		||||
	if event.TextMessage.Sender != nil {
 | 
			
		||||
		sender = event.TextMessage.Sender.Name
 | 
			
		||||
	}
 | 
			
		||||
	// If the text message is received before receiving a ServerSync
 | 
			
		||||
	// and UserState, Client.Self or Self.Channel are nil
 | 
			
		||||
	if event.Client.Self == nil || event.Client.Self.Channel == nil {
 | 
			
		||||
		b.Log.Warn("Connection bootstrap not finished, discarding text message")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// Convert Mumble HTML messages to markdown
 | 
			
		||||
	parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Error(err)
 | 
			
		||||
	}
 | 
			
		||||
	now := time.Now().UTC()
 | 
			
		||||
	for i, part := range parts {
 | 
			
		||||
		// Construct matterbridge message and pass on to the gateway
 | 
			
		||||
		rmsg := config.Message{
 | 
			
		||||
			Channel:  strconv.FormatUint(uint64(event.Client.Self.Channel.ID), 10),
 | 
			
		||||
			Username: sender,
 | 
			
		||||
			UserID:   sender + "@" + b.Host,
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
		}
 | 
			
		||||
		if part.Image == nil {
 | 
			
		||||
			rmsg.Text = part.Text
 | 
			
		||||
		} else {
 | 
			
		||||
			fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + part.FileExtension
 | 
			
		||||
			rmsg.Extra = make(map[string][]interface{})
 | 
			
		||||
			if err = helper.HandleDownloadSize(b.Log, &rmsg, fname, int64(len(part.Image)), b.General); err != nil {
 | 
			
		||||
				b.Log.WithError(err).Warn("not including image in message")
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			helper.HandleDownloadData(b.Log, &rmsg, fname, "", "", &part.Image, b.General)
 | 
			
		||||
		}
 | 
			
		||||
		b.Log.Debugf("Sending message to gateway: %+v", rmsg)
 | 
			
		||||
		b.Remote <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
 | 
			
		||||
	// Set the user's "bio"/comment
 | 
			
		||||
	if comment := b.GetString("UserComment"); comment != "" && event.Client.Self != nil {
 | 
			
		||||
		event.Client.Self.SetComment(comment)
 | 
			
		||||
	}
 | 
			
		||||
	// No need to talk or listen
 | 
			
		||||
	event.Client.Self.SetSelfDeafened(true)
 | 
			
		||||
	event.Client.Self.SetSelfMuted(true)
 | 
			
		||||
	// if the Channel variable is set, this is a reconnect -> rejoin channel
 | 
			
		||||
	if b.Channel != nil {
 | 
			
		||||
		if err := b.doJoin(event.Client, *b.Channel); err != nil {
 | 
			
		||||
			b.Log.Error(err)
 | 
			
		||||
		}
 | 
			
		||||
		b.Remote <- config.Message{
 | 
			
		||||
			Username: "system",
 | 
			
		||||
			Text:     "rejoin",
 | 
			
		||||
			Channel:  "",
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			Event:    config.EventRejoinChannels,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleJoinLeave(event *gumble.UserChangeEvent) {
 | 
			
		||||
	// Ignore events happening before setup is done
 | 
			
		||||
	if b.Channel == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if b.GetBool("nosendjoinpart") {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("Received gumble user change event: %+v", event)
 | 
			
		||||
 | 
			
		||||
	text := ""
 | 
			
		||||
	switch {
 | 
			
		||||
	case event.Type&gumble.UserChangeKicked > 0:
 | 
			
		||||
		text = " was kicked"
 | 
			
		||||
	case event.Type&gumble.UserChangeBanned > 0:
 | 
			
		||||
		text = " was banned"
 | 
			
		||||
	case event.Type&gumble.UserChangeDisconnected > 0:
 | 
			
		||||
		if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
 | 
			
		||||
			text = " left"
 | 
			
		||||
		}
 | 
			
		||||
	case event.Type&gumble.UserChangeConnected > 0:
 | 
			
		||||
		if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
 | 
			
		||||
			text = " joined"
 | 
			
		||||
		}
 | 
			
		||||
	case event.Type&gumble.UserChangeChannel > 0:
 | 
			
		||||
		// Treat Mumble channel changes the same as connects/disconnects; as far as matterbridge is concerned, they are identical
 | 
			
		||||
		if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
 | 
			
		||||
			text = " joined"
 | 
			
		||||
		} else {
 | 
			
		||||
			text = " left"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if text != "" {
 | 
			
		||||
		b.Remote <- config.Message{
 | 
			
		||||
			Username: "system",
 | 
			
		||||
			Text:     event.User.Name + text,
 | 
			
		||||
			Channel:  strconv.FormatUint(uint64(*b.Channel), 10),
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			Event:    config.EventJoinLeave,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleUserModified(event *gumble.UserChangeEvent) {
 | 
			
		||||
	// Ignore events happening before setup is done
 | 
			
		||||
	if b.Channel == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if event.Type&gumble.UserChangeChannel > 0 {
 | 
			
		||||
		// Someone attempted to move the user out of the configured channel; attempt to join back
 | 
			
		||||
		if err := b.doJoin(event.Client, *b.Channel); err != nil {
 | 
			
		||||
			b.Log.Error(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) {
 | 
			
		||||
	// The UserChangeEvent is used for both the gumble client itself as well as other clients
 | 
			
		||||
	if event.User != event.Client.Self {
 | 
			
		||||
		// other users
 | 
			
		||||
		b.handleJoinLeave(event)
 | 
			
		||||
	} else {
 | 
			
		||||
		// gumble user
 | 
			
		||||
		b.handleUserModified(event)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) {
 | 
			
		||||
	b.connected <- *event
 | 
			
		||||
}
 | 
			
		||||
@@ -1,143 +0,0 @@
 | 
			
		||||
package bmumble
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/mattn/godown"
 | 
			
		||||
	"github.com/vincent-petithory/dataurl"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MessagePart struct {
 | 
			
		||||
	Text          string
 | 
			
		||||
	FileExtension string
 | 
			
		||||
	Image         []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) decodeImage(uri string, parts *[]MessagePart) error {
 | 
			
		||||
	// Decode the data:image/... URI
 | 
			
		||||
	image, err := dataurl.DecodeString(uri)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.WithError(err).Info("No image extracted")
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	// Determine the file extensions for that image
 | 
			
		||||
	ext, err := mime.ExtensionsByType(image.MediaType.ContentType())
 | 
			
		||||
	if err != nil || len(ext) == 0 {
 | 
			
		||||
		b.Log.WithError(err).Infof("No file extension registered for MIME type '%s'", image.MediaType.ContentType())
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	// Add the image to the MessagePart slice
 | 
			
		||||
	*parts = append(*parts, MessagePart{"", ext[0], image.Data})
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) tokenize(t *string) ([]MessagePart, error) {
 | 
			
		||||
	// `^(.*?)` matches everything before the image
 | 
			
		||||
	// `!\[[^\]]*\]\(` matches the `]+)` matches the data: URI used by Mumble
 | 
			
		||||
	// `\)` matches the closing parenthesis after the URI
 | 
			
		||||
	// `(.*)$` matches the remaining text to be examined in the next iteration
 | 
			
		||||
	p := regexp.MustCompile(`^(?ms)(.*?)!\[[^\]]*\]\((data:image\/[^)]+)\)(.*)$`)
 | 
			
		||||
	remaining := *t
 | 
			
		||||
	var parts []MessagePart
 | 
			
		||||
	for {
 | 
			
		||||
		tokens := p.FindStringSubmatch(remaining)
 | 
			
		||||
		if tokens == nil {
 | 
			
		||||
			// no match -> remaining string is non-image text
 | 
			
		||||
			pre := strings.TrimSpace(remaining)
 | 
			
		||||
			if len(pre) > 0 {
 | 
			
		||||
				parts = append(parts, MessagePart{pre, "", nil})
 | 
			
		||||
			}
 | 
			
		||||
			return parts, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// tokens[1] is the text before the image
 | 
			
		||||
		if len(tokens[1]) > 0 {
 | 
			
		||||
			pre := strings.TrimSpace(tokens[1])
 | 
			
		||||
			parts = append(parts, MessagePart{pre, "", nil})
 | 
			
		||||
		}
 | 
			
		||||
		// tokens[2] is the image URL
 | 
			
		||||
		uri, err := dataurl.UnescapeToString(strings.TrimSpace(strings.ReplaceAll(tokens[2], " ", "")))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.WithError(err).Info("URL unescaping failed")
 | 
			
		||||
			remaining = strings.TrimSpace(tokens[3])
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		err = b.decodeImage(uri, &parts)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.WithError(err).Info("Decoding the image failed")
 | 
			
		||||
		}
 | 
			
		||||
		// tokens[3] is the text after the image, processed in the next iteration
 | 
			
		||||
		remaining = strings.TrimSpace(tokens[3])
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) convertHTMLtoMarkdown(html string) ([]MessagePart, error) {
 | 
			
		||||
	var sb strings.Builder
 | 
			
		||||
	err := godown.Convert(&sb, strings.NewReader(html), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	markdown := sb.String()
 | 
			
		||||
	b.Log.Debugf("### to markdown: %s", markdown)
 | 
			
		||||
	return b.tokenize(&markdown)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) extractFiles(msg *config.Message) []config.Message {
 | 
			
		||||
	var messages []config.Message
 | 
			
		||||
	if msg.Extra == nil || len(msg.Extra["file"]) == 0 {
 | 
			
		||||
		return messages
 | 
			
		||||
	}
 | 
			
		||||
	// Create a separate message for each file
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
		imsg := config.Message{
 | 
			
		||||
			Channel:   msg.Channel,
 | 
			
		||||
			Username:  msg.Username,
 | 
			
		||||
			UserID:    msg.UserID,
 | 
			
		||||
			Account:   msg.Account,
 | 
			
		||||
			Protocol:  msg.Protocol,
 | 
			
		||||
			Timestamp: msg.Timestamp,
 | 
			
		||||
			Event:     "mumble_image",
 | 
			
		||||
		}
 | 
			
		||||
		// If no data is present for the file, send a link instead
 | 
			
		||||
		if fi.Data == nil || len(*fi.Data) == 0 {
 | 
			
		||||
			if len(fi.URL) > 0 {
 | 
			
		||||
				imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL)
 | 
			
		||||
				messages = append(messages, imsg)
 | 
			
		||||
			} else {
 | 
			
		||||
				b.Log.Infof("Not forwarding file without local data")
 | 
			
		||||
			}
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		mimeType := http.DetectContentType(*fi.Data)
 | 
			
		||||
		// Mumble only supports images natively, send a link instead
 | 
			
		||||
		if !strings.HasPrefix(mimeType, "image/") {
 | 
			
		||||
			if len(fi.URL) > 0 {
 | 
			
		||||
				imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL)
 | 
			
		||||
				messages = append(messages, imsg)
 | 
			
		||||
			} else {
 | 
			
		||||
				b.Log.Infof("Not forwarding file of type %s", mimeType)
 | 
			
		||||
			}
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0])
 | 
			
		||||
		// Build data:image/...;base64,... style image URL and embed image directly into the message
 | 
			
		||||
		du := dataurl.New(*fi.Data, mimeType)
 | 
			
		||||
		dataURL, err := du.MarshalText()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.WithError(err).Infof("Image Serialization into data URL failed (type: %s, length: %d)", mimeType, len(*fi.Data))
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		imsg.Text = fmt.Sprintf(`<img src="%s"/>`, dataURL)
 | 
			
		||||
		messages = append(messages, imsg)
 | 
			
		||||
	}
 | 
			
		||||
	// Remove files from original message
 | 
			
		||||
	msg.Extra["file"] = nil
 | 
			
		||||
	return messages
 | 
			
		||||
}
 | 
			
		||||
@@ -1,263 +0,0 @@
 | 
			
		||||
package bmumble
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"layeh.com/gumble/gumble"
 | 
			
		||||
	"layeh.com/gumble/gumbleutil"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	stripmd "github.com/writeas/go-strip-markdown"
 | 
			
		||||
 | 
			
		||||
	// We need to import the 'data' package as an implicit dependency.
 | 
			
		||||
	// See: https://godoc.org/github.com/paulrosania/go-charset/charset
 | 
			
		||||
	_ "github.com/paulrosania/go-charset/data"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Bmumble struct {
 | 
			
		||||
	client             *gumble.Client
 | 
			
		||||
	Nick               string
 | 
			
		||||
	Host               string
 | 
			
		||||
	Channel            *uint32
 | 
			
		||||
	local              chan config.Message
 | 
			
		||||
	running            chan error
 | 
			
		||||
	connected          chan gumble.DisconnectEvent
 | 
			
		||||
	serverConfigUpdate chan gumble.ServerConfigEvent
 | 
			
		||||
	serverConfig       gumble.ServerConfigEvent
 | 
			
		||||
	tlsConfig          tls.Config
 | 
			
		||||
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	b := &Bmumble{}
 | 
			
		||||
	b.Config = cfg
 | 
			
		||||
	b.Nick = b.GetString("Nick")
 | 
			
		||||
	b.local = make(chan config.Message)
 | 
			
		||||
	b.running = make(chan error)
 | 
			
		||||
	b.connected = make(chan gumble.DisconnectEvent)
 | 
			
		||||
	b.serverConfigUpdate = make(chan gumble.ServerConfigEvent)
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) Connect() error {
 | 
			
		||||
	b.Log.Infof("Connecting %s", b.GetString("Server"))
 | 
			
		||||
	host, portstr, err := net.SplitHostPort(b.GetString("Server"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.Host = host
 | 
			
		||||
	_, err = strconv.Atoi(portstr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = b.buildTLSConfig(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	go b.doSend()
 | 
			
		||||
	go b.connectLoop()
 | 
			
		||||
	err = <-b.running
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) Disconnect() error {
 | 
			
		||||
	return b.client.Disconnect()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	cid, err := strconv.ParseUint(channel.Name, 10, 32)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	channelID := uint32(cid)
 | 
			
		||||
	if b.Channel != nil && *b.Channel != channelID {
 | 
			
		||||
		b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel)
 | 
			
		||||
		return errors.New("the Mumble bridge can only join a single channel")
 | 
			
		||||
	}
 | 
			
		||||
	b.Channel = &channelID
 | 
			
		||||
	return b.doJoin(b.client, channelID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) Send(msg config.Message) (string, error) {
 | 
			
		||||
	// Only process text messages
 | 
			
		||||
	b.Log.Debugf("=> Received local message %#v", msg)
 | 
			
		||||
	if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	attachments := b.extractFiles(&msg)
 | 
			
		||||
	b.local <- msg
 | 
			
		||||
	for _, a := range attachments {
 | 
			
		||||
		b.local <- a
 | 
			
		||||
	}
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) buildTLSConfig() error {
 | 
			
		||||
	b.tlsConfig = tls.Config{}
 | 
			
		||||
	// Load TLS client certificate keypair required for registered user authentication
 | 
			
		||||
	if cpath := b.GetString("TLSClientCertificate"); cpath != "" {
 | 
			
		||||
		if ckey := b.GetString("TLSClientKey"); ckey != "" {
 | 
			
		||||
			cert, err := tls.LoadX509KeyPair(cpath, ckey)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			b.tlsConfig.Certificates = []tls.Certificate{cert}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// Load TLS CA used for server verification.  If not provided, the Go system trust anchor is used
 | 
			
		||||
	if capath := b.GetString("TLSCACertificate"); capath != "" {
 | 
			
		||||
		ca, err := ioutil.ReadFile(capath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		b.tlsConfig.RootCAs = x509.NewCertPool()
 | 
			
		||||
		b.tlsConfig.RootCAs.AppendCertsFromPEM(ca)
 | 
			
		||||
	}
 | 
			
		||||
	b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) connectLoop() {
 | 
			
		||||
	firstConnect := true
 | 
			
		||||
	for {
 | 
			
		||||
		err := b.doConnect()
 | 
			
		||||
		if firstConnect {
 | 
			
		||||
			b.running <- err
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("Connection to server failed: %#v", err)
 | 
			
		||||
			if firstConnect {
 | 
			
		||||
				break
 | 
			
		||||
			} else {
 | 
			
		||||
				b.Log.Info("Retrying in 10s")
 | 
			
		||||
				time.Sleep(10 * time.Second)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		firstConnect = false
 | 
			
		||||
		d := <-b.connected
 | 
			
		||||
		switch d.Type {
 | 
			
		||||
		case gumble.DisconnectError:
 | 
			
		||||
			b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String)
 | 
			
		||||
			continue
 | 
			
		||||
		case gumble.DisconnectKicked:
 | 
			
		||||
			b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String)
 | 
			
		||||
			continue
 | 
			
		||||
		case gumble.DisconnectBanned:
 | 
			
		||||
			b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String)
 | 
			
		||||
			close(b.connected)
 | 
			
		||||
			close(b.running)
 | 
			
		||||
			return
 | 
			
		||||
		case gumble.DisconnectUser:
 | 
			
		||||
			b.Log.Infof("Disconnect successful")
 | 
			
		||||
			close(b.connected)
 | 
			
		||||
			close(b.running)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) doConnect() error {
 | 
			
		||||
	// Create new gumble config and attach event handlers
 | 
			
		||||
	gumbleConfig := gumble.NewConfig()
 | 
			
		||||
	gumbleConfig.Attach(gumbleutil.Listener{
 | 
			
		||||
		ServerConfig: b.handleServerConfig,
 | 
			
		||||
		TextMessage:  b.handleTextMessage,
 | 
			
		||||
		Connect:      b.handleConnect,
 | 
			
		||||
		Disconnect:   b.handleDisconnect,
 | 
			
		||||
		UserChange:   b.handleUserChange,
 | 
			
		||||
	})
 | 
			
		||||
	gumbleConfig.Username = b.GetString("Nick")
 | 
			
		||||
	if password := b.GetString("Password"); password != "" {
 | 
			
		||||
		gumbleConfig.Password = password
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	registerNullCodecAsOpus()
 | 
			
		||||
	client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.client = client
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error {
 | 
			
		||||
	channel, ok := client.Channels[channelID]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return fmt.Errorf("no channel with ID %d", channelID)
 | 
			
		||||
	}
 | 
			
		||||
	client.Self.Move(channel)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) doSend() {
 | 
			
		||||
	// Message sending loop that makes sure server-side
 | 
			
		||||
	// restrictions and client-side message traits don't conflict
 | 
			
		||||
	// with each other.
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case serverConfig := <-b.serverConfigUpdate:
 | 
			
		||||
			b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength)
 | 
			
		||||
			b.serverConfig = serverConfig
 | 
			
		||||
		case msg := <-b.local:
 | 
			
		||||
			b.processMessage(&msg)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bmumble) processMessage(msg *config.Message) {
 | 
			
		||||
	b.Log.Debugf("Processing message %s", msg.Text)
 | 
			
		||||
 | 
			
		||||
	allowHTML := true
 | 
			
		||||
	if b.serverConfig.AllowHTML != nil {
 | 
			
		||||
		allowHTML = *b.serverConfig.AllowHTML
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If this is a specially generated image message, send it unmodified
 | 
			
		||||
	if msg.Event == "mumble_image" {
 | 
			
		||||
		if allowHTML {
 | 
			
		||||
			b.client.Self.Channel.Send(msg.Username+msg.Text, false)
 | 
			
		||||
		} else {
 | 
			
		||||
			b.Log.Info("Can't send image, server does not allow HTML messages")
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Don't process empty messages
 | 
			
		||||
	if len(msg.Text) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// If HTML is allowed, convert markdown into HTML, otherwise strip markdown
 | 
			
		||||
	if allowHTML {
 | 
			
		||||
		msg.Text = helper.ParseMarkdown(msg.Text)
 | 
			
		||||
	} else {
 | 
			
		||||
		msg.Text = stripmd.Strip(msg.Text)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If there is a maximum message length, split and truncate the lines
 | 
			
		||||
	var msgLines []string
 | 
			
		||||
	if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
 | 
			
		||||
		msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
 | 
			
		||||
	} else {
 | 
			
		||||
		msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
 | 
			
		||||
	}
 | 
			
		||||
	// Send the individual lines
 | 
			
		||||
	for i := range msgLines {
 | 
			
		||||
		// Remove unnecessary newline character, since either way we're sending it as individual lines
 | 
			
		||||
		msgLines[i] = strings.TrimSuffix(msgLines[i], "\n")
 | 
			
		||||
		b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,295 +0,0 @@
 | 
			
		||||
package nctalk
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
 | 
			
		||||
	"gomod.garykim.dev/nc-talk/ocs"
 | 
			
		||||
	"gomod.garykim.dev/nc-talk/room"
 | 
			
		||||
	"gomod.garykim.dev/nc-talk/user"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Btalk struct {
 | 
			
		||||
	user  *user.TalkUser
 | 
			
		||||
	rooms []Broom
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	return &Btalk{Config: cfg}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Broom struct {
 | 
			
		||||
	room      *room.TalkRoom
 | 
			
		||||
	ctx       context.Context
 | 
			
		||||
	ctxCancel context.CancelFunc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) Connect() error {
 | 
			
		||||
	b.Log.Info("Connecting")
 | 
			
		||||
	tconfig := &user.TalkUserConfig{
 | 
			
		||||
		TLSConfig: &tls.Config{
 | 
			
		||||
			InsecureSkipVerify: b.GetBool("SkipTLSVerify"), //nolint:gosec
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	var err error
 | 
			
		||||
	b.user, err = user.NewUser(b.GetString("Server"), b.GetString("Login"), b.GetString("Password"), tconfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Error("Config could not be used")
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	_, err = b.user.Capabilities()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Error("Cannot Connect")
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Info("Connected")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) Disconnect() error {
 | 
			
		||||
	for _, r := range b.rooms {
 | 
			
		||||
		r.ctxCancel()
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	tr, err := room.NewTalkRoom(b.user, channel.Name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	newRoom := Broom{
 | 
			
		||||
		room: tr,
 | 
			
		||||
	}
 | 
			
		||||
	newRoom.ctx, newRoom.ctxCancel = context.WithCancel(context.Background())
 | 
			
		||||
	c, err := newRoom.room.ReceiveMessages(newRoom.ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.rooms = append(b.rooms, newRoom)
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		for msg := range c {
 | 
			
		||||
			msg := msg
 | 
			
		||||
 | 
			
		||||
			if msg.Error != nil {
 | 
			
		||||
				b.Log.Errorf("Fatal message poll error: %s\n", msg.Error)
 | 
			
		||||
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Ignore messages that are from the bot user
 | 
			
		||||
			if msg.ActorID == b.user.User || msg.ActorType == "bridged" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Handle deleting messages
 | 
			
		||||
			if msg.MessageType == ocs.MessageSystem && msg.Parent != nil && msg.Parent.MessageType == ocs.MessageDelete {
 | 
			
		||||
				b.handleDeletingMessage(&msg, &newRoom)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Handle sending messages
 | 
			
		||||
			if msg.MessageType == ocs.MessageComment {
 | 
			
		||||
				b.handleSendingMessage(&msg, &newRoom)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) Send(msg config.Message) (string, error) {
 | 
			
		||||
	r := b.getRoom(msg.Channel)
 | 
			
		||||
	if r == nil {
 | 
			
		||||
		b.Log.Errorf("Could not find room for %v", msg.Channel)
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Standard Message Send
 | 
			
		||||
	if msg.Event == "" {
 | 
			
		||||
		// Handle sending files if they are included
 | 
			
		||||
		err := b.handleSendingFile(&msg, r)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("Could not send files in message to room %v from %v: %v", msg.Channel, msg.Username, err)
 | 
			
		||||
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		sentMessage, err := b.sendText(r, &msg, msg.Text)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err)
 | 
			
		||||
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
		return strconv.Itoa(sentMessage.ID), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Message Deletion
 | 
			
		||||
	if msg.Event == config.EventMsgDelete {
 | 
			
		||||
		messageID, err := strconv.Atoi(msg.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		data, err := r.room.DeleteMessage(messageID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return strconv.Itoa(data.ID), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Message is not a type that is currently supported
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) getRoom(token string) *Broom {
 | 
			
		||||
	for _, r := range b.rooms {
 | 
			
		||||
		if r.room.Token == token {
 | 
			
		||||
			return &r
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) sendText(r *Broom, msg *config.Message, text string) (*ocs.TalkRoomMessageData, error) {
 | 
			
		||||
	messageToSend := &room.Message{Message: msg.Username + text}
 | 
			
		||||
 | 
			
		||||
	if b.GetBool("SeparateDisplayName") {
 | 
			
		||||
		messageToSend.Message = text
 | 
			
		||||
		messageToSend.ActorDisplayName = msg.Username
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return r.room.SendComplexMessage(messageToSend)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error {
 | 
			
		||||
	for _, parameter := range message.MessageParameters {
 | 
			
		||||
		if parameter.Type == ocs.ROSTypeFile {
 | 
			
		||||
			// Get the file
 | 
			
		||||
			file, err := b.user.DownloadFile(parameter.Path)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if mmsg.Extra == nil {
 | 
			
		||||
				mmsg.Extra = make(map[string][]interface{})
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			mmsg.Extra["file"] = append(mmsg.Extra["file"], config.FileInfo{
 | 
			
		||||
				Name:   parameter.Name,
 | 
			
		||||
				Data:   file,
 | 
			
		||||
				Size:   int64(len(*file)),
 | 
			
		||||
				Avatar: false,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) handleSendingFile(msg *config.Message, r *Broom) error {
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
		if fi.URL == "" {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		message := ""
 | 
			
		||||
		if fi.Comment != "" {
 | 
			
		||||
			message += fi.Comment + " "
 | 
			
		||||
		}
 | 
			
		||||
		message += fi.URL
 | 
			
		||||
		_, err := b.sendText(r, msg, message)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) handleSendingMessage(msg *ocs.TalkRoomMessageData, r *Broom) {
 | 
			
		||||
	remoteMessage := config.Message{
 | 
			
		||||
		Text:     formatRichObjectString(msg.Message, msg.MessageParameters),
 | 
			
		||||
		Channel:  r.room.Token,
 | 
			
		||||
		Username: DisplayName(msg, b.guestSuffix()),
 | 
			
		||||
		UserID:   msg.ActorID,
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
	}
 | 
			
		||||
	// It is possible for the ID to not be set on older versions of Talk so we only set it if
 | 
			
		||||
	// the ID is not blank
 | 
			
		||||
	if msg.ID != 0 {
 | 
			
		||||
		remoteMessage.ID = strconv.Itoa(msg.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle Files
 | 
			
		||||
	err := b.handleFiles(&remoteMessage, msg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Error handling file: %#v", msg)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", remoteMessage)
 | 
			
		||||
	b.Remote <- remoteMessage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) handleDeletingMessage(msg *ocs.TalkRoomMessageData, r *Broom) {
 | 
			
		||||
	remoteMessage := config.Message{
 | 
			
		||||
		Event:   config.EventMsgDelete,
 | 
			
		||||
		Text:    config.EventMsgDelete,
 | 
			
		||||
		Channel: r.room.Token,
 | 
			
		||||
		ID:      strconv.Itoa(msg.Parent.ID),
 | 
			
		||||
		Account: b.Account,
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("<= Message being deleted is %#v", remoteMessage)
 | 
			
		||||
	b.Remote <- remoteMessage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btalk) guestSuffix() string {
 | 
			
		||||
	guestSuffix := " (Guest)"
 | 
			
		||||
	if b.IsKeySet("GuestSuffix") {
 | 
			
		||||
		guestSuffix = b.GetString("GuestSuffix")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return guestSuffix
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785
 | 
			
		||||
func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string {
 | 
			
		||||
	for id, parameter := range parameters {
 | 
			
		||||
		text := parameter.Name
 | 
			
		||||
 | 
			
		||||
		switch parameter.Type {
 | 
			
		||||
		case ocs.ROSTypeUser, ocs.ROSTypeGroup:
 | 
			
		||||
			text = "@" + text
 | 
			
		||||
		case ocs.ROSTypeFile:
 | 
			
		||||
			if parameter.Link != "" {
 | 
			
		||||
				text = parameter.Name
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		message = strings.ReplaceAll(message, "{"+id+"}", text)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return message
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DisplayName(msg *ocs.TalkRoomMessageData, suffix string) string {
 | 
			
		||||
	if msg.ActorType == ocs.ActorGuest {
 | 
			
		||||
		if msg.ActorDisplayName == "" {
 | 
			
		||||
			return "Guest"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return msg.ActorDisplayName + suffix
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return msg.ActorDisplayName
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,7 @@
 | 
			
		||||
package brocketchat
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Brocketchat) handleRocket() {
 | 
			
		||||
@@ -42,26 +38,8 @@ func (b *Brocketchat) handleRocketHook(messages chan *config.Message) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Brocketchat) handleStatusEvent(ev models.Message, rmsg *config.Message) bool {
 | 
			
		||||
	switch ev.Type {
 | 
			
		||||
	case "":
 | 
			
		||||
		// this is a normal message, no processing needed
 | 
			
		||||
		// return true so the message is not dropped
 | 
			
		||||
		return true
 | 
			
		||||
	case sUserJoined, sUserLeft:
 | 
			
		||||
		rmsg.Event = config.EventJoinLeave
 | 
			
		||||
		return true
 | 
			
		||||
	case sRoomChangedTopic:
 | 
			
		||||
		rmsg.Event = config.EventTopicChange
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("Dropping message with unknown type: %s", ev.Type)
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
 | 
			
		||||
	for message := range b.messageChan {
 | 
			
		||||
		message := message
 | 
			
		||||
		// skip messages with same ID, apparently messages get duplicated for an unknown reason
 | 
			
		||||
		if _, ok := b.cache.Get(message.ID); ok {
 | 
			
		||||
			continue
 | 
			
		||||
@@ -80,51 +58,11 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
 | 
			
		||||
			Account:  b.Account,
 | 
			
		||||
			UserID:   message.User.ID,
 | 
			
		||||
			ID:       message.ID,
 | 
			
		||||
			Extra:    make(map[string][]interface{}),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.handleAttachments(&message, rmsg)
 | 
			
		||||
 | 
			
		||||
		// handleStatusEvent returns false if the message should be dropped
 | 
			
		||||
		// in that case it is probably some modification to the channel we do not want to relay
 | 
			
		||||
		if b.handleStatusEvent(m, rmsg) {
 | 
			
		||||
			messages <- rmsg
 | 
			
		||||
		}
 | 
			
		||||
		messages <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Brocketchat) handleAttachments(message *models.Message, rmsg *config.Message) {
 | 
			
		||||
	if rmsg.Text == "" {
 | 
			
		||||
		for _, attachment := range message.Attachments {
 | 
			
		||||
			if attachment.Title != "" {
 | 
			
		||||
				rmsg.Text = attachment.Title + "\n"
 | 
			
		||||
			}
 | 
			
		||||
			if attachment.Title != "" && attachment.Text != "" {
 | 
			
		||||
				rmsg.Text += "\n"
 | 
			
		||||
			}
 | 
			
		||||
			if attachment.Text != "" {
 | 
			
		||||
				rmsg.Text += attachment.Text
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := range message.Attachments {
 | 
			
		||||
		if err := b.handleDownloadFile(rmsg, &message.Attachments[i]); err != nil {
 | 
			
		||||
			b.Log.Errorf("Could not download incoming file: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Brocketchat) handleDownloadFile(rmsg *config.Message, file *models.Attachment) error {
 | 
			
		||||
	downloadURL := b.GetString("server") + file.TitleLink
 | 
			
		||||
	data, err := helper.DownloadFileAuthRocket(downloadURL, b.user.Token, b.user.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("download %s failed %#v", downloadURL, err)
 | 
			
		||||
	}
 | 
			
		||||
	helper.HandleDownloadData(b.Log, rmsg, file.Title, rmsg.Text, downloadURL, data, b.General)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Brocketchat) handleUploadFile(msg *config.Message) error {
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
 
 | 
			
		||||
@@ -58,9 +58,6 @@ func (b *Brocketchat) doConnectWebhookURL() error {
 | 
			
		||||
func (b *Brocketchat) apiLogin() error {
 | 
			
		||||
	b.Log.Debugf("handling apiLogin()")
 | 
			
		||||
	credentials := &models.UserCredentials{Email: b.GetString("login"), Password: b.GetString("password")}
 | 
			
		||||
	if b.GetString("Token") != "" {
 | 
			
		||||
		credentials = &models.UserCredentials{ID: b.GetString("Login"), Token: b.GetString("Token")}
 | 
			
		||||
	}
 | 
			
		||||
	myURL, err := url.Parse(b.GetString("server"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
 
 | 
			
		||||
@@ -29,12 +29,6 @@ type Brocketchat struct {
 | 
			
		||||
	sync.RWMutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	sUserJoined       = "uj"
 | 
			
		||||
	sUserLeft         = "ul"
 | 
			
		||||
	sRoomChangedTopic = "room_changed_topic"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	newCache, err := lru.New(100)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -114,11 +108,6 @@ func (b *Brocketchat) Send(msg config.Message) (string, error) {
 | 
			
		||||
	msg.Channel = strings.TrimPrefix(msg.Channel, "#")
 | 
			
		||||
	channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel}
 | 
			
		||||
 | 
			
		||||
	// Make a action /me of the message
 | 
			
		||||
	if msg.Event == config.EventUserAction {
 | 
			
		||||
		msg.Text = "_" + msg.Text + "_"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Delete message
 | 
			
		||||
	if msg.Event == config.EventMsgDelete {
 | 
			
		||||
		if msg.ID == "" {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,18 @@
 | 
			
		||||
package bslack
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/slack-go/slack"
 | 
			
		||||
	"github.com/nlopes/slack"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ErrEventIgnored is for events that should be ignored
 | 
			
		||||
var ErrEventIgnored = errors.New("this event message should ignored")
 | 
			
		||||
 | 
			
		||||
func (b *Bslack) handleSlack() {
 | 
			
		||||
	messages := make(chan *config.Message)
 | 
			
		||||
	if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
 | 
			
		||||
	if b.GetString(incomingWebhookConfig) != "" {
 | 
			
		||||
		b.Log.Debugf("Choosing webhooks based receiving")
 | 
			
		||||
		go b.handleMatterHook(messages)
 | 
			
		||||
	} else {
 | 
			
		||||
@@ -27,15 +23,13 @@ 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 &&
 | 
			
		||||
			message.Event != config.EventFileDelete {
 | 
			
		||||
		if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete {
 | 
			
		||||
			b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
 | 
			
		||||
			// cleanup the message
 | 
			
		||||
			message.Text = b.replaceMention(message.Text)
 | 
			
		||||
			message.Text = b.replaceVariable(message.Text)
 | 
			
		||||
			message.Text = b.replaceChannel(message.Text)
 | 
			
		||||
			message.Text = b.replaceURL(message.Text)
 | 
			
		||||
			message.Text = b.replaceb0rkedMarkDown(message.Text)
 | 
			
		||||
			message.Text = html.UnescapeString(message.Text)
 | 
			
		||||
 | 
			
		||||
			// Add the avatar
 | 
			
		||||
@@ -49,7 +43,7 @@ func (b *Bslack) handleSlack() {
 | 
			
		||||
 | 
			
		||||
func (b *Bslack) handleSlackClient(messages chan *config.Message) {
 | 
			
		||||
	for msg := range b.rtm.IncomingEvents {
 | 
			
		||||
		if msg.Type != sUserTyping && msg.Type != sHello && msg.Type != sLatencyReport {
 | 
			
		||||
		if msg.Type != sUserTyping && msg.Type != sLatencyReport {
 | 
			
		||||
			b.Log.Debugf("== Receiving event %#v", msg.Data)
 | 
			
		||||
		}
 | 
			
		||||
		switch ev := msg.Data.(type) {
 | 
			
		||||
@@ -58,9 +52,7 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			rmsg, err := b.handleTypingEvent(ev)
 | 
			
		||||
			if err == ErrEventIgnored {
 | 
			
		||||
				continue
 | 
			
		||||
			} else if err != nil {
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Errorf("%#v", err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
@@ -77,13 +69,6 @@ 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:
 | 
			
		||||
@@ -101,10 +86,8 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
 | 
			
		||||
			b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
 | 
			
		||||
		case *slack.MemberJoinedChannelEvent:
 | 
			
		||||
			b.users.populateUser(ev.User)
 | 
			
		||||
		case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent:
 | 
			
		||||
		case *slack.LatencyReport:
 | 
			
		||||
			continue
 | 
			
		||||
		case *slack.UserChangeEvent:
 | 
			
		||||
			b.users.invalidateUser(ev.User.ID)
 | 
			
		||||
		default:
 | 
			
		||||
			b.Log.Debugf("Unhandled incoming event: %T", ev)
 | 
			
		||||
		}
 | 
			
		||||
@@ -140,11 +123,11 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check for our callback ID
 | 
			
		||||
	hasOurCallbackID := false
 | 
			
		||||
	if len(ev.Blocks.BlockSet) == 1 {
 | 
			
		||||
		block, ok := ev.Blocks.BlockSet[0].(*slack.SectionBlock)
 | 
			
		||||
		hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid
 | 
			
		||||
	// Skip any messages that we made ourselves or from 'slackbot' (see #527).
 | 
			
		||||
	if ev.Username == sSlackBotUser ||
 | 
			
		||||
		(b.rtm != nil && ev.Username == b.si.User.Name) ||
 | 
			
		||||
		(len(ev.Attachments) > 0 && ev.Attachments[0].CallbackID == "matterbridge_"+b.uuid) {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ev.SubMessage != nil {
 | 
			
		||||
@@ -159,16 +142,6 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
 | 
			
		||||
		if ev.SubType == "message_replied" && ev.Hidden {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
		if len(ev.SubMessage.Blocks.BlockSet) == 1 {
 | 
			
		||||
			block, ok := ev.SubMessage.Blocks.BlockSet[0].(*slack.SectionBlock)
 | 
			
		||||
			hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Skip any messages that we made ourselves or from 'slackbot' (see #527).
 | 
			
		||||
	if ev.Username == sSlackBotUser ||
 | 
			
		||||
		(b.rtm != nil && ev.Username == b.si.User.Name) || hasOurCallbackID {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(ev.Files) > 0 {
 | 
			
		||||
@@ -230,26 +203,6 @@ 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:
 | 
			
		||||
@@ -282,13 +235,6 @@ 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 {
 | 
			
		||||
@@ -297,15 +243,12 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
 | 
			
		||||
 | 
			
		||||
	// See if we have some text in the attachments.
 | 
			
		||||
	if rmsg.Text == "" {
 | 
			
		||||
		for i, attach := range ev.Attachments {
 | 
			
		||||
		for _, attach := range ev.Attachments {
 | 
			
		||||
			if attach.Text != "" {
 | 
			
		||||
				if attach.Title != "" {
 | 
			
		||||
					rmsg.Text = getMessageTitle(&ev.Attachments[i])
 | 
			
		||||
					rmsg.Text = attach.Title + "\n"
 | 
			
		||||
				}
 | 
			
		||||
				rmsg.Text += attach.Text
 | 
			
		||||
				if attach.Footer != "" {
 | 
			
		||||
					rmsg.Text += "\n\n" + attach.Footer
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				rmsg.Text = attach.Fallback
 | 
			
		||||
			}
 | 
			
		||||
@@ -319,8 +262,6 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
 | 
			
		||||
 | 
			
		||||
	// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
 | 
			
		||||
	for i := range ev.Files {
 | 
			
		||||
		// keep reference in cache on which channel we added this file
 | 
			
		||||
		b.cache.Add(cfileDownloadChannel+ev.Files[i].ID, ev.Channel)
 | 
			
		||||
		if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
 | 
			
		||||
			b.Log.Errorf("Could not download incoming file: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
@@ -328,9 +269,6 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) {
 | 
			
		||||
	if ev.User == b.si.User.ID {
 | 
			
		||||
		return nil, ErrEventIgnored
 | 
			
		||||
	}
 | 
			
		||||
	channelInfo, err := b.channels.getChannelByID(ev.Channel)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
@@ -370,7 +308,7 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retr
 | 
			
		||||
	// that the comment is not duplicated.
 | 
			
		||||
	comment := rmsg.Text
 | 
			
		||||
	rmsg.Text = ""
 | 
			
		||||
	helper.HandleDownloadData2(b.Log, rmsg, file.Name, file.ID, comment, file.URLPrivateDownload, data, b.General)
 | 
			
		||||
	helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,8 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/nlopes/slack"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/slack-go/slack"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
 | 
			
		||||
@@ -87,9 +87,6 @@ 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -127,7 +124,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)(?:: (.*))?`)
 | 
			
		||||
)
 | 
			
		||||
@@ -181,35 +178,12 @@ 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 {
 | 
			
		||||
	return urlRE.ReplaceAllString(text, "[${2}](${1})")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bslack) replaceb0rkedMarkDown(text string) string {
 | 
			
		||||
	// taken from https://github.com/mattermost/mattermost-server/blob/master/app/slackimport.go
 | 
			
		||||
	//
 | 
			
		||||
	regexReplaceAllString := []struct {
 | 
			
		||||
		regex *regexp.Regexp
 | 
			
		||||
		rpl   string
 | 
			
		||||
	}{
 | 
			
		||||
		// bold
 | 
			
		||||
		{
 | 
			
		||||
			regexp.MustCompile(`(^|[\s.;,])\*(\S[^*\n]+)\*`),
 | 
			
		||||
			"$1**$2**",
 | 
			
		||||
		},
 | 
			
		||||
		// strikethrough
 | 
			
		||||
		{
 | 
			
		||||
			regexp.MustCompile(`(^|[\s.;,])\~(\S[^~\n]+)\~`),
 | 
			
		||||
			"$1~~$2~~",
 | 
			
		||||
		},
 | 
			
		||||
		// single paragraph blockquote
 | 
			
		||||
		// Slack converts > character to >
 | 
			
		||||
		{
 | 
			
		||||
			regexp.MustCompile(`(?sm)^>`),
 | 
			
		||||
			">",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, rule := range regexReplaceAllString {
 | 
			
		||||
		text = rule.regex.ReplaceAllString(text, rule.rpl)
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterhook"
 | 
			
		||||
	"github.com/slack-go/slack"
 | 
			
		||||
	"github.com/nlopes/slack"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BLegacy struct {
 | 
			
		||||
@@ -13,9 +13,7 @@ type BLegacy struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLegacy(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	b := &BLegacy{Bslack: newBridge(cfg)}
 | 
			
		||||
	b.legacy = true
 | 
			
		||||
	return b
 | 
			
		||||
	return &BLegacy{Bslack: newBridge(cfg)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *BLegacy) Connect() error {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,9 @@ import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/42wim/matterbridge/matterhook"
 | 
			
		||||
	lru "github.com/hashicorp/golang-lru"
 | 
			
		||||
	"github.com/hashicorp/golang-lru"
 | 
			
		||||
	"github.com/nlopes/slack"
 | 
			
		||||
	"github.com/rs/xid"
 | 
			
		||||
	"github.com/slack-go/slack"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Bslack struct {
 | 
			
		||||
@@ -32,29 +32,26 @@ type Bslack struct {
 | 
			
		||||
 | 
			
		||||
	channels *channels
 | 
			
		||||
	users    *users
 | 
			
		||||
	legacy   bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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"
 | 
			
		||||
	cfileDownloadChannel = "file_download_channel"
 | 
			
		||||
	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"
 | 
			
		||||
 | 
			
		||||
	tokenConfig           = "Token"
 | 
			
		||||
	incomingWebhookConfig = "WebhookBindAddress"
 | 
			
		||||
@@ -65,7 +62,6 @@ const (
 | 
			
		||||
	editSuffixConfig      = "EditSuffix"
 | 
			
		||||
	iconURLConfig         = "iconurl"
 | 
			
		||||
	noSendJoinConfig      = "nosendjoinpart"
 | 
			
		||||
	messageLength         = 3000
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
@@ -155,18 +151,6 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// try to join a channel when in legacy
 | 
			
		||||
	if b.legacy {
 | 
			
		||||
		_, _, _, err := b.sc.JoinConversation(channel.Name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			switch err.Error() {
 | 
			
		||||
			case "name_taken", "restricted_action":
 | 
			
		||||
			case "default":
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.channels.populateChannels(false)
 | 
			
		||||
 | 
			
		||||
	channelInfo, err := b.channels.getChannel(channel.Name)
 | 
			
		||||
@@ -179,8 +163,7 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
		channel.Name = channelInfo.Name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// we can't join a channel unless we are using legacy tokens #651
 | 
			
		||||
	if !channelInfo.IsMember && !b.legacy {
 | 
			
		||||
	if !channelInfo.IsMember {
 | 
			
		||||
		return fmt.Errorf("slack integration that matterbridge is using is not member of channel '%s', please add it manually", channelInfo.Name)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
@@ -196,7 +179,6 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
 | 
			
		||||
		b.Log.Debugf("=> Receiving %#v", msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msg.Text = helper.ClipMessage(msg.Text, messageLength, b.GetString("MessageClipped"))
 | 
			
		||||
	msg.Text = b.replaceCodeFence(msg.Text)
 | 
			
		||||
 | 
			
		||||
	// Make a action /me of the message
 | 
			
		||||
@@ -205,7 +187,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Use webhook to send the message
 | 
			
		||||
	if b.GetString(outgoingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
 | 
			
		||||
	if b.GetString(outgoingWebhookConfig) != "" {
 | 
			
		||||
		return "", b.sendWebhook(msg)
 | 
			
		||||
	}
 | 
			
		||||
	return b.sendRTM(msg)
 | 
			
		||||
@@ -300,7 +282,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle prefix hint for unthreaded messages.
 | 
			
		||||
	if msg.ParentNotFound() {
 | 
			
		||||
	if msg.ParentID == "msg-parent-not-found" {
 | 
			
		||||
		msg.ParentID = ""
 | 
			
		||||
		msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
 | 
			
		||||
	}
 | 
			
		||||
@@ -321,7 +303,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Upload a file if it exists.
 | 
			
		||||
	if len(msg.Extra) > 0 {
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		extraMsgs := helper.HandleExtra(&msg, b.General)
 | 
			
		||||
		for i := range extraMsgs {
 | 
			
		||||
			rmsg := &extraMsgs[i]
 | 
			
		||||
@@ -332,7 +314,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// Upload files if necessary (from Slack, Telegram or Mattermost).
 | 
			
		||||
		return b.uploadFile(&msg, channelInfo.ID)
 | 
			
		||||
		b.uploadFile(&msg, channelInfo.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post message.
 | 
			
		||||
@@ -411,6 +393,7 @@ func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (b
 | 
			
		||||
	}
 | 
			
		||||
	messageOptions := b.prepareMessageOptions(msg)
 | 
			
		||||
	for {
 | 
			
		||||
		messageOptions = append(messageOptions, slack.MsgOptionText(msg.Text, false))
 | 
			
		||||
		_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return true, nil
 | 
			
		||||
@@ -429,6 +412,11 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
	messageOptions := b.prepareMessageOptions(msg)
 | 
			
		||||
	messageOptions = append(
 | 
			
		||||
		messageOptions,
 | 
			
		||||
		slack.MsgOptionText(msg.Text, false),
 | 
			
		||||
		slack.MsgOptionEnableLinkUnfurl(),
 | 
			
		||||
	)
 | 
			
		||||
	for {
 | 
			
		||||
		_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
@@ -443,8 +431,7 @@ 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) (string, error) {
 | 
			
		||||
	var messageID string
 | 
			
		||||
func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi, ok := f.(config.FileInfo)
 | 
			
		||||
		if !ok {
 | 
			
		||||
@@ -461,7 +448,7 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) (string, erro
 | 
			
		||||
		b.cache.Add("filename"+fi.Name, ts)
 | 
			
		||||
		initialComment := fmt.Sprintf("File from %s", msg.Username)
 | 
			
		||||
		if fi.Comment != "" {
 | 
			
		||||
			initialComment += fmt.Sprintf(" with comment: %s", fi.Comment)
 | 
			
		||||
			initialComment += fmt.Sprintf("with comment: %s", fi.Comment)
 | 
			
		||||
		}
 | 
			
		||||
		res, err := b.sc.UploadFile(slack.FileUploadParameters{
 | 
			
		||||
			Reader:          bytes.NewReader(*fi.Data),
 | 
			
		||||
@@ -472,22 +459,13 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) (string, erro
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("uploadfile %#v", err)
 | 
			
		||||
			return "", err
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		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 {
 | 
			
		||||
@@ -504,6 +482,8 @@ func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var attachments []slack.Attachment
 | 
			
		||||
	// add a callback ID so we can see we created it
 | 
			
		||||
	attachments = append(attachments, slack.Attachment{CallbackID: "matterbridge_" + b.uuid})
 | 
			
		||||
	// add file attachments
 | 
			
		||||
	attachments = append(attachments, b.createAttach(msg.Extra)...)
 | 
			
		||||
	// add slack attachments (from another slack bridge)
 | 
			
		||||
@@ -514,19 +494,6 @@ func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var opts []slack.MsgOption
 | 
			
		||||
	opts = append(opts,
 | 
			
		||||
		// provide regular text field (fallback used in Slack notifications, etc.)
 | 
			
		||||
		slack.MsgOptionText(msg.Text, false),
 | 
			
		||||
 | 
			
		||||
		// add a callback ID so we can see we created it
 | 
			
		||||
		slack.MsgOptionBlocks(slack.NewSectionBlock(
 | 
			
		||||
			slack.NewTextBlockObject(slack.MarkdownType, msg.Text, false, false),
 | 
			
		||||
			nil, nil,
 | 
			
		||||
			slack.SectionBlockOptionBlockID("matterbridge_"+b.uuid),
 | 
			
		||||
		)),
 | 
			
		||||
 | 
			
		||||
		slack.MsgOptionEnableLinkUnfurl(),
 | 
			
		||||
	)
 | 
			
		||||
	opts = append(opts, slack.MsgOptionAttachments(attachments...))
 | 
			
		||||
	opts = append(opts, slack.MsgOptionPostMessageParameters(params))
 | 
			
		||||
	return opts
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,8 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/nlopes/slack"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/slack-go/slack"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const minimumRefreshInterval = 10 * time.Second
 | 
			
		||||
@@ -113,12 +113,6 @@ func (b *users) populateUser(userID string) {
 | 
			
		||||
	b.users[userID] = user
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *users) invalidateUser(userID string) {
 | 
			
		||||
	b.usersMutex.Lock()
 | 
			
		||||
	defer b.usersMutex.Unlock()
 | 
			
		||||
	delete(b.users, userID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *users) populateUsers(wait bool) {
 | 
			
		||||
	b.refreshMutex.Lock()
 | 
			
		||||
	if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
 | 
			
		||||
@@ -289,9 +283,8 @@ func (b *channels) populateChannels(wait bool) {
 | 
			
		||||
	// We only retrieve public and private channels, not IMs
 | 
			
		||||
	// and MPIMs as those do not have a channel name.
 | 
			
		||||
	queryParams := &slack.GetConversationsParameters{
 | 
			
		||||
		ExcludeArchived: true,
 | 
			
		||||
		ExcludeArchived: "true",
 | 
			
		||||
		Types:           []string{"public_channel,private_channel"},
 | 
			
		||||
		Limit:           1000,
 | 
			
		||||
	}
 | 
			
		||||
	for {
 | 
			
		||||
		channels, nextCursor, err := b.sc.GetConversations(queryParams)
 | 
			
		||||
 
 | 
			
		||||
@@ -130,10 +130,6 @@ func (b *Bsshchat) handleSSHChat() error {
 | 
			
		||||
			if strings.Contains(b.r.Text(), "Rate limiting is in effect") {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			// skip our own messages
 | 
			
		||||
			if !strings.HasPrefix(b.r.Text(), "["+b.GetString("Nick")+"] \x1b") {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			res := strings.Split(stripPrompt(b.r.Text()), ":")
 | 
			
		||||
			if res[0] == "-> Set theme" {
 | 
			
		||||
				wait = false
 | 
			
		||||
 
 | 
			
		||||
@@ -85,7 +85,7 @@ func (b *Bsteam) handleEvents() {
 | 
			
		||||
 | 
			
		||||
func (b *Bsteam) handleLogOnFailed(e *steam.LogOnFailedEvent, myLoginInfo *steam.LogOnDetails) error {
 | 
			
		||||
	switch e.Result {
 | 
			
		||||
	case steamlang.EResult_AccountLoginDeniedNeedTwoFactor:
 | 
			
		||||
	case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode:
 | 
			
		||||
		b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:")
 | 
			
		||||
		var code string
 | 
			
		||||
		fmt.Scanf("%s", &code)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +1,21 @@
 | 
			
		||||
package btelegram
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unicode/utf16"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/davecgh/go-spew/spew"
 | 
			
		||||
	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
 | 
			
		||||
	"github.com/go-telegram-bot-api/telegram-bot-api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
 | 
			
		||||
	// handle channels
 | 
			
		||||
	if posted != nil {
 | 
			
		||||
		if posted.Text == "/chatId" {
 | 
			
		||||
			chatID := strconv.FormatInt(posted.Chat.ID, 10)
 | 
			
		||||
 | 
			
		||||
			_, err := b.Send(config.Message{
 | 
			
		||||
				Channel: chatID,
 | 
			
		||||
				Text:    fmt.Sprintf("ID of this chat: %s", chatID),
 | 
			
		||||
			})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Warnf("Unable to send chatID to %s", chatID)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			message = posted
 | 
			
		||||
			rmsg.Text = message.Text
 | 
			
		||||
		}
 | 
			
		||||
		message = posted
 | 
			
		||||
		rmsg.Text = message.Text
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// edited channel message
 | 
			
		||||
@@ -53,40 +38,22 @@ func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message
 | 
			
		||||
 | 
			
		||||
// handleForwarded handles forwarded messages
 | 
			
		||||
func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) {
 | 
			
		||||
	if message.ForwardDate == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if message.ForwardFromChat != nil && message.ForwardFrom == nil {
 | 
			
		||||
		rmsg.Text = "Forwarded from " + message.ForwardFromChat.Title + ": " + rmsg.Text
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if message.ForwardFrom == nil {
 | 
			
		||||
		rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	usernameForward := ""
 | 
			
		||||
	if b.GetBool("UseFirstName") {
 | 
			
		||||
		usernameForward = message.ForwardFrom.FirstName
 | 
			
		||||
	}
 | 
			
		||||
	if b.GetBool("UseFullName") {
 | 
			
		||||
		usernameForward = message.ForwardFrom.FirstName + " " + message.ForwardFrom.LastName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if usernameForward == "" {
 | 
			
		||||
		usernameForward = message.ForwardFrom.UserName
 | 
			
		||||
		if usernameForward == "" {
 | 
			
		||||
	if message.ForwardFrom != nil {
 | 
			
		||||
		usernameForward := ""
 | 
			
		||||
		if b.GetBool("UseFirstName") {
 | 
			
		||||
			usernameForward = message.ForwardFrom.FirstName
 | 
			
		||||
		}
 | 
			
		||||
		if usernameForward == "" {
 | 
			
		||||
			usernameForward = message.ForwardFrom.UserName
 | 
			
		||||
			if usernameForward == "" {
 | 
			
		||||
				usernameForward = message.ForwardFrom.FirstName
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if usernameForward == "" {
 | 
			
		||||
			usernameForward = unknownUser
 | 
			
		||||
		}
 | 
			
		||||
		rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if usernameForward == "" {
 | 
			
		||||
		usernameForward = unknownUser
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleQuoting handles quoting of previous messages
 | 
			
		||||
@@ -97,9 +64,6 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
 | 
			
		||||
			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 == "" {
 | 
			
		||||
@@ -111,11 +75,7 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
 | 
			
		||||
			usernameReply = unknownUser
 | 
			
		||||
		}
 | 
			
		||||
		if !b.GetBool("QuoteDisable") {
 | 
			
		||||
			quote := message.ReplyToMessage.Text
 | 
			
		||||
			if quote == "" {
 | 
			
		||||
				quote = message.ReplyToMessage.Caption
 | 
			
		||||
			}
 | 
			
		||||
			rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, quote)
 | 
			
		||||
			rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -123,13 +83,10 @@ 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.FormatInt(message.From.ID, 10)
 | 
			
		||||
		rmsg.UserID = strconv.Itoa(message.From.ID)
 | 
			
		||||
		if b.GetBool("UseFirstName") {
 | 
			
		||||
			rmsg.Username = message.From.FirstName
 | 
			
		||||
		}
 | 
			
		||||
		if b.GetBool("UseFullName") {
 | 
			
		||||
			rmsg.Username = message.From.FirstName + " " + message.From.LastName
 | 
			
		||||
		}
 | 
			
		||||
		if rmsg.Username == "" {
 | 
			
		||||
			rmsg.Username = message.From.UserName
 | 
			
		||||
			if rmsg.Username == "" {
 | 
			
		||||
@@ -137,33 +94,11 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// only download avatars if we have a place to upload them (configured mediaserver)
 | 
			
		||||
		if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") {
 | 
			
		||||
		if b.General.MediaServerUpload != "" {
 | 
			
		||||
			b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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") {
 | 
			
		||||
			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)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if we really didn't find a username, set it to unknown
 | 
			
		||||
	if rmsg.Username == "" {
 | 
			
		||||
		rmsg.Username = unknownUser
 | 
			
		||||
@@ -176,14 +111,10 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
 | 
			
		||||
 | 
			
		||||
		if update.Message == nil && update.ChannelPost == nil &&
 | 
			
		||||
			update.EditedMessage == nil && update.EditedChannelPost == nil {
 | 
			
		||||
			b.Log.Info("Received event without messages, skipping.")
 | 
			
		||||
			b.Log.Error("Getting nil messages, this shouldn't happen.")
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if b.GetInt("debuglevel") == 1 {
 | 
			
		||||
			spew.Dump(update.Message)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var message *tgbotapi.Message
 | 
			
		||||
 | 
			
		||||
		rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
 | 
			
		||||
@@ -203,14 +134,6 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
 | 
			
		||||
		rmsg.ID = strconv.Itoa(message.MessageID)
 | 
			
		||||
		rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
 | 
			
		||||
 | 
			
		||||
		// preserve threading from telegram reply
 | 
			
		||||
		if message.ReplyToMessage != nil {
 | 
			
		||||
			rmsg.ParentID = strconv.Itoa(message.ReplyToMessage.MessageID)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// handle entities (adding URLs)
 | 
			
		||||
		b.handleEntities(&rmsg, message)
 | 
			
		||||
 | 
			
		||||
		// handle username
 | 
			
		||||
		b.handleUsername(&rmsg, message)
 | 
			
		||||
 | 
			
		||||
@@ -226,12 +149,14 @@ 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 {
 | 
			
		||||
			// Comment the next line out due to avoid removing empty lines in Telegram
 | 
			
		||||
			// rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
 | 
			
		||||
			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.FormatInt(message.From.ID, 10), b.General)
 | 
			
		||||
				rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
 | 
			
		||||
@@ -244,73 +169,39 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
 | 
			
		||||
// handleDownloadAvatar downloads the avatar of userid from channel
 | 
			
		||||
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
 | 
			
		||||
// logs an error message if it fails
 | 
			
		||||
func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) {
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		Username: "system",
 | 
			
		||||
		Text:     "avatar",
 | 
			
		||||
		Channel:  channel,
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		UserID:   strconv.FormatInt(userid, 10),
 | 
			
		||||
		Event:    config.EventAvatarDownload,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
	}
 | 
			
		||||
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
 | 
			
		||||
	rmsg := config.Message{Username: "system",
 | 
			
		||||
		Text:    "avatar",
 | 
			
		||||
		Channel: channel,
 | 
			
		||||
		Account: b.Account,
 | 
			
		||||
		UserID:  strconv.Itoa(userid),
 | 
			
		||||
		Event:   config.EventAvatarDownload,
 | 
			
		||||
		Extra:   make(map[string][]interface{})}
 | 
			
		||||
 | 
			
		||||
	if _, ok := b.avatarMap[strconv.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 _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
 | 
			
		||||
		photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Error(err)
 | 
			
		||||
			return
 | 
			
		||||
			b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
 | 
			
		||||
		}
 | 
			
		||||
		data, err := helper.DownloadFile(url)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("download %s failed %#v", url, err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
 | 
			
		||||
		b.Remote <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) {
 | 
			
		||||
	format := b.GetString("MediaConvertTgs")
 | 
			
		||||
	if helper.SupportsFormat(format) {
 | 
			
		||||
		b.Log.Debugf("Format supported by %s, converting %v", helper.LottieBackend(), name)
 | 
			
		||||
	} else {
 | 
			
		||||
		// Otherwise, no conversion was requested. Trying to run the usual webp
 | 
			
		||||
		// converter would fail, because '.tgs.webp' is actually a gzipped JSON
 | 
			
		||||
		// file, and has nothing to do with WebP.
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	err := helper.ConvertTgsToX(data, format, b.Log)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("conversion failed: %v", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		*name = strings.Replace(*name, "tgs.webp", format, 1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		if len(photos.Photos) > 0 {
 | 
			
		||||
			photo := photos.Photos[0][0]
 | 
			
		||||
			url := b.getFileDirectURL(photo.FileID)
 | 
			
		||||
			name := strconv.Itoa(userid) + ".png"
 | 
			
		||||
			b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) {
 | 
			
		||||
	if b.GetBool("MediaConvertWebPToPNG") {
 | 
			
		||||
		b.Log.Debugf("WebP to PNG conversion enabled, converting %v", name)
 | 
			
		||||
		err := helper.ConvertWebPToPNG(data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("conversion failed: %v", err)
 | 
			
		||||
		} else {
 | 
			
		||||
			*name = strings.Replace(*name, ".webp", ".png", 1)
 | 
			
		||||
			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
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -338,7 +229,7 @@ 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
 | 
			
		||||
		photos := *message.Photo
 | 
			
		||||
		size = photos[len(photos)-1].FileSize
 | 
			
		||||
		text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
 | 
			
		||||
	}
 | 
			
		||||
@@ -362,18 +253,15 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.HasSuffix(name, ".tgs.webp") {
 | 
			
		||||
		b.maybeConvertTgs(&name, data)
 | 
			
		||||
	} else if strings.HasSuffix(name, ".webp") {
 | 
			
		||||
		b.maybeConvertWebp(&name, data)
 | 
			
		||||
	if strings.HasSuffix(name, ".webp") && b.GetBool("MediaConvertWebPToPNG") {
 | 
			
		||||
		b.Log.Debugf("WebP to PNG conversion enabled, converting %s", name)
 | 
			
		||||
		err := helper.ConvertWebPToPNG(data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("conversion failed: %s", err)
 | 
			
		||||
		} else {
 | 
			
		||||
			name = strings.Replace(name, ".webp", ".png", 1)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// rename .oga to .ogg  https://github.com/42wim/matterbridge/issues/906#issuecomment-741793512
 | 
			
		||||
	if strings.HasSuffix(name, ".oga") && message.Audio != nil {
 | 
			
		||||
		name = strings.Replace(name, ".oga", ".ogg", 1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -385,7 +273,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) && !strings.HasSuffix(name, ".webm") {
 | 
			
		||||
	if suffix != "" && !strings.HasSuffix(name, suffix) {
 | 
			
		||||
		name += suffix
 | 
			
		||||
	}
 | 
			
		||||
	text := " " + url
 | 
			
		||||
@@ -397,15 +285,11 @@ 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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfg := tgbotapi.NewDeleteMessage(chatid, msgid)
 | 
			
		||||
	_, err = b.c.Request(cfg)
 | 
			
		||||
 | 
			
		||||
	_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
 | 
			
		||||
	return "", err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -427,9 +311,6 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error
 | 
			
		||||
	case "Markdown":
 | 
			
		||||
		b.Log.Debug("Using mode markdown")
 | 
			
		||||
		m.ParseMode = tgbotapi.ModeMarkdown
 | 
			
		||||
	case MarkdownV2:
 | 
			
		||||
		b.Log.Debug("Using mode MarkdownV2")
 | 
			
		||||
		m.ParseMode = MarkdownV2
 | 
			
		||||
	}
 | 
			
		||||
	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
 | 
			
		||||
		b.Log.Debug("Using mode HTML - nick only")
 | 
			
		||||
@@ -443,57 +324,31 @@ 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, parentID int) (string, error) {
 | 
			
		||||
	var media []interface{}
 | 
			
		||||
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
 | 
			
		||||
	var c tgbotapi.Chattable
 | 
			
		||||
	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))
 | 
			
		||||
		re := regexp.MustCompile(".(jpg|png)$")
 | 
			
		||||
		if re.MatchString(fi.Name) {
 | 
			
		||||
			c = tgbotapi.NewPhotoUpload(chatid, file)
 | 
			
		||||
		} else {
 | 
			
		||||
			c = tgbotapi.NewDocumentUpload(chatid, file)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch filepath.Ext(fi.Name) {
 | 
			
		||||
		case ".jpg", ".jpe", ".png":
 | 
			
		||||
			pc := tgbotapi.NewInputMediaPhoto(file)
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
		_, err := b.c.Send(c)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("file upload failed: %#v", err)
 | 
			
		||||
		}
 | 
			
		||||
		if fi.Comment != "" {
 | 
			
		||||
			if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil {
 | 
			
		||||
				b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err)
 | 
			
		||||
			}
 | 
			
		||||
			media = append(media, pc)
 | 
			
		||||
		case ".mp4", ".m4v":
 | 
			
		||||
			vc := tgbotapi.NewInputMediaVideo(file)
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			}
 | 
			
		||||
			media = append(media, vc)
 | 
			
		||||
		case ".mp3", ".oga":
 | 
			
		||||
			ac := tgbotapi.NewInputMediaAudio(file)
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			}
 | 
			
		||||
			media = append(media, ac)
 | 
			
		||||
		case ".ogg":
 | 
			
		||||
			voc := tgbotapi.NewVoice(chatid, file)
 | 
			
		||||
			voc.Caption, voc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			voc.ReplyToMessageID = parentID
 | 
			
		||||
			res, err := b.c.Send(voc)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return "", err
 | 
			
		||||
			}
 | 
			
		||||
			return strconv.Itoa(res.MessageID), nil
 | 
			
		||||
		default:
 | 
			
		||||
			dc := tgbotapi.NewInputMediaDocument(file)
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
 | 
			
		||||
			}
 | 
			
		||||
			media = append(media, dc)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b.sendMediaFiles(msg, chatid, parentID, media)
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
 | 
			
		||||
@@ -501,14 +356,6 @@ func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string
 | 
			
		||||
	if format == "" {
 | 
			
		||||
		format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
 | 
			
		||||
	}
 | 
			
		||||
	quoteMessagelength := len([]rune(quoteMessage))
 | 
			
		||||
	if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") {
 | 
			
		||||
		runes := []rune(quoteMessage)
 | 
			
		||||
		quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")])
 | 
			
		||||
		if quoteMessagelength > b.GetInt("QuoteLengthLimit") {
 | 
			
		||||
			quoteMessage += "..."
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	format = strings.Replace(format, "{MESSAGE}", message, -1)
 | 
			
		||||
	format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1)
 | 
			
		||||
	format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)
 | 
			
		||||
@@ -520,61 +367,16 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa
 | 
			
		||||
	if message.Entities == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	indexMovedBy := 0
 | 
			
		||||
	prevLinkOffset := -1
 | 
			
		||||
 | 
			
		||||
	for _, e := range message.Entities {
 | 
			
		||||
 | 
			
		||||
		asRunes := utf16.Encode([]rune(rmsg.Text))
 | 
			
		||||
 | 
			
		||||
	// for now only do URL replacements
 | 
			
		||||
	for _, e := range *message.Entities {
 | 
			
		||||
		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 offset+e.Length > len(utfEncodedString) {
 | 
			
		||||
				b.Log.Errorf("entity length is too long %d > %d", offset+e.Length, len(utfEncodedString))
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			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
 | 
			
		||||
			link := rmsg.Text[e.Offset : e.Offset+e.Length]
 | 
			
		||||
			rmsg.Text = strings.Replace(rmsg.Text, link, url.String(), 1)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package btelegram
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"html"
 | 
			
		||||
 | 
			
		||||
	"github.com/russross/blackfriday"
 | 
			
		||||
)
 | 
			
		||||
@@ -23,16 +24,10 @@ 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(string(text))
 | 
			
		||||
	out.WriteString(html.EscapeString(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)
 | 
			
		||||
}
 | 
			
		||||
@@ -47,10 +42,6 @@ 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,23 +1,20 @@
 | 
			
		||||
package btelegram
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"log"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
 | 
			
		||||
	"github.com/go-telegram-bot-api/telegram-bot-api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	unknownUser = "unknown"
 | 
			
		||||
	HTMLFormat  = "HTML"
 | 
			
		||||
	HTMLNick    = "htmlnick"
 | 
			
		||||
	MarkdownV2  = "MarkdownV2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Btelegram struct {
 | 
			
		||||
@@ -27,16 +24,6 @@ type Btelegram struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	tgsConvertFormat := cfg.GetString("MediaConvertTgs")
 | 
			
		||||
	if tgsConvertFormat != "" {
 | 
			
		||||
		err := helper.CanConvertTgsToX()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s does not appear to work:\n%#v", tgsConvertFormat, helper.LottieBackend(), err)
 | 
			
		||||
		}
 | 
			
		||||
		if !helper.SupportsFormat(tgsConvertFormat) {
 | 
			
		||||
			log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s doesn't support it.", tgsConvertFormat, helper.LottieBackend())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &Btelegram{Config: cfg, avatarMap: make(map[string]string)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -50,7 +37,11 @@ func (b *Btelegram) Connect() error {
 | 
			
		||||
	}
 | 
			
		||||
	u := tgbotapi.NewUpdate(0)
 | 
			
		||||
	u.Timeout = 60
 | 
			
		||||
	updates := b.c.GetUpdatesChan(u)
 | 
			
		||||
	updates, err := b.c.GetUpdatesChan(u)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Debugf("%#v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	go b.handleRecv(updates)
 | 
			
		||||
	return nil
 | 
			
		||||
@@ -64,28 +55,6 @@ func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TGGetParseMode(b *Btelegram, username string, text string) (textout string, parsemode string) {
 | 
			
		||||
	textout = username + text
 | 
			
		||||
	if b.GetString("MessageFormat") == HTMLFormat {
 | 
			
		||||
		b.Log.Debug("Using mode HTML")
 | 
			
		||||
		parsemode = tgbotapi.ModeHTML
 | 
			
		||||
	}
 | 
			
		||||
	if b.GetString("MessageFormat") == "Markdown" {
 | 
			
		||||
		b.Log.Debug("Using mode markdown")
 | 
			
		||||
		parsemode = tgbotapi.ModeMarkdown
 | 
			
		||||
	}
 | 
			
		||||
	if b.GetString("MessageFormat") == MarkdownV2 {
 | 
			
		||||
		b.Log.Debug("Using mode MarkdownV2")
 | 
			
		||||
		parsemode = MarkdownV2
 | 
			
		||||
	}
 | 
			
		||||
	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
 | 
			
		||||
		b.Log.Debug("Using mode HTML - nick only")
 | 
			
		||||
		textout = username + html.EscapeString(text)
 | 
			
		||||
		parsemode = tgbotapi.ModeHTML
 | 
			
		||||
	}
 | 
			
		||||
	return textout, parsemode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) Send(msg config.Message) (string, error) {
 | 
			
		||||
	b.Log.Debugf("=> Receiving %#v", msg)
 | 
			
		||||
 | 
			
		||||
@@ -101,7 +70,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.GetString("MessageFormat") == HTMLFormat {
 | 
			
		||||
		msg.Text = makeHTML(html.EscapeString(msg.Text))
 | 
			
		||||
		msg.Text = makeHTML(msg.Text)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Delete message
 | 
			
		||||
@@ -109,27 +78,16 @@ 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, parentID); msgErr != nil {
 | 
			
		||||
				b.Log.Errorf("sendMessage failed: %s", msgErr)
 | 
			
		||||
			if _, err := b.sendMessage(chatid, rmsg.Username, rmsg.Text); err != nil {
 | 
			
		||||
				b.Log.Errorf("sendMessage failed: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// check if we have files to upload (from slack, telegram or mattermost)
 | 
			
		||||
		if len(msg.Extra["file"]) > 0 {
 | 
			
		||||
			return b.handleUploadFile(&msg, chatid, parentID)
 | 
			
		||||
			b.handleUploadFile(&msg, chatid)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -139,14 +97,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post normal message
 | 
			
		||||
	// TODO: recheck it.
 | 
			
		||||
	// Ignore empty text field needs for prevent double messages from whatsapp to telegram
 | 
			
		||||
	// when sending media with text caption
 | 
			
		||||
	if msg.Text != "" {
 | 
			
		||||
		return b.sendMessage(chatid, msg.Username, msg.Text, parentID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "", nil
 | 
			
		||||
	return b.sendMessage(chatid, msg.Username, msg.Text)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) getFileDirectURL(id string) string {
 | 
			
		||||
@@ -157,12 +108,22 @@ func (b *Btelegram) getFileDirectURL(id string) string {
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) sendMessage(chatid int64, username, text string, parentID int) (string, error) {
 | 
			
		||||
func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) {
 | 
			
		||||
	m := tgbotapi.NewMessage(chatid, "")
 | 
			
		||||
	m.Text, m.ParseMode = TGGetParseMode(b, username, text)
 | 
			
		||||
	m.ReplyToMessageID = parentID
 | 
			
		||||
	m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
 | 
			
		||||
 | 
			
		||||
	m.Text = username + text
 | 
			
		||||
	if b.GetString("MessageFormat") == HTMLFormat {
 | 
			
		||||
		b.Log.Debug("Using mode HTML")
 | 
			
		||||
		m.ParseMode = tgbotapi.ModeHTML
 | 
			
		||||
	}
 | 
			
		||||
	if b.GetString("MessageFormat") == "Markdown" {
 | 
			
		||||
		b.Log.Debug("Using mode markdown")
 | 
			
		||||
		m.ParseMode = tgbotapi.ModeMarkdown
 | 
			
		||||
	}
 | 
			
		||||
	if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
 | 
			
		||||
		b.Log.Debug("Using mode HTML - nick only")
 | 
			
		||||
		m.Text = username + html.EscapeString(text)
 | 
			
		||||
		m.ParseMode = tgbotapi.ModeHTML
 | 
			
		||||
	}
 | 
			
		||||
	res, err := b.c.Send(m)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
@@ -170,29 +131,6 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string, parentID in
 | 
			
		||||
	return strconv.Itoa(res.MessageID), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sendMediaFiles native upload media files via media group
 | 
			
		||||
func (b *Btelegram) sendMediaFiles(msg *config.Message, chatid int64, parentID int, media []interface{}) (string, error) {
 | 
			
		||||
	if len(media) == 0 {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
	mg := tgbotapi.MediaGroupConfig{ChatID: chatid, ChannelUsername: msg.Username, Media: media, ReplyToMessageID: parentID}
 | 
			
		||||
	messages, err := b.c.SendMediaGroup(mg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	// return first message id
 | 
			
		||||
	return strconv.Itoa(messages[0].MessageID), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// intParentID return integer parent id for telegram message
 | 
			
		||||
func (b *Btelegram) intParentID(parentID string) (int, error) {
 | 
			
		||||
	pid, err := strconv.Atoi(parentID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return pid, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) {
 | 
			
		||||
	fi := msg.Extra["file"][0].(config.FileInfo)
 | 
			
		||||
	/* if we have a sha we have successfully uploaded the file to the media server,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										333
									
								
								bridge/vk/vk.go
									
									
									
									
									
								
							
							
						
						
									
										333
									
								
								bridge/vk/vk.go
									
									
									
									
									
								
							@@ -1,333 +0,0 @@
 | 
			
		||||
package bvk
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
 | 
			
		||||
	"github.com/SevereCloud/vksdk/v2/api"
 | 
			
		||||
	"github.com/SevereCloud/vksdk/v2/events"
 | 
			
		||||
	longpoll "github.com/SevereCloud/vksdk/v2/longpoll-bot"
 | 
			
		||||
	"github.com/SevereCloud/vksdk/v2/object"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	audioMessage = "audio_message"
 | 
			
		||||
	document     = "doc"
 | 
			
		||||
	photo        = "photo"
 | 
			
		||||
	video        = "video"
 | 
			
		||||
	graffiti     = "graffiti"
 | 
			
		||||
	sticker      = "sticker"
 | 
			
		||||
	wall         = "wall"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type user struct {
 | 
			
		||||
	lastname, firstname, avatar string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Bvk struct {
 | 
			
		||||
	c            *api.VK
 | 
			
		||||
	lp           *longpoll.LongPoll
 | 
			
		||||
	usernamesMap map[int]user // cache of user names and avatar URLs
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	return &Bvk{usernamesMap: make(map[int]user), Config: cfg}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) Connect() error {
 | 
			
		||||
	b.Log.Info("Connecting")
 | 
			
		||||
	b.c = api.NewVK(b.GetString("Token"))
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	b.lp, err = longpoll.NewLongPollCommunity(b.c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Debugf("%#v", err)
 | 
			
		||||
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) {
 | 
			
		||||
		b.handleMessage(obj.Message, false)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		err := b.lp.Run()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.WithError(err).Fatal("Enable longpoll in group management")
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) Disconnect() error {
 | 
			
		||||
	b.lp.Shutdown()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) Send(msg config.Message) (string, error) {
 | 
			
		||||
	b.Log.Debugf("=> Receiving %#v", msg)
 | 
			
		||||
 | 
			
		||||
	peerID, err := strconv.Atoi(msg.Channel)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	params := api.Params{}
 | 
			
		||||
 | 
			
		||||
	text := msg.Username + msg.Text
 | 
			
		||||
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		if len(msg.Extra["file"]) > 0 {
 | 
			
		||||
			// generate attachments string
 | 
			
		||||
			attachment, urls := b.uploadFiles(msg.Extra, peerID)
 | 
			
		||||
			params["attachment"] = attachment
 | 
			
		||||
			text += urls
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	params["message"] = text
 | 
			
		||||
 | 
			
		||||
	if msg.ID == "" {
 | 
			
		||||
		// New message
 | 
			
		||||
		params["random_id"] = time.Now().Unix()
 | 
			
		||||
		params["peer_ids"] = msg.Channel
 | 
			
		||||
 | 
			
		||||
		res, e := b.c.MessagesSendPeerIDs(params)
 | 
			
		||||
		if e != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return strconv.Itoa(res[0].ConversationMessageID), nil
 | 
			
		||||
	}
 | 
			
		||||
	// Edit message
 | 
			
		||||
	messageID, err := strconv.ParseInt(msg.ID, 10, 64)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	params["peer_id"] = peerID
 | 
			
		||||
	params["conversation_message_id"] = messageID
 | 
			
		||||
 | 
			
		||||
	_, err = b.c.MessagesEdit(params)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return msg.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) getUser(id int) user {
 | 
			
		||||
	u, found := b.usernamesMap[id]
 | 
			
		||||
	if !found {
 | 
			
		||||
		b.Log.Debug("Fetching username for ", id)
 | 
			
		||||
 | 
			
		||||
		if id >= 0 {
 | 
			
		||||
			result, _ := b.c.UsersGet(api.Params{
 | 
			
		||||
				"user_ids": id,
 | 
			
		||||
				"fields":   "photo_200",
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			resUser := result[0]
 | 
			
		||||
			u = user{lastname: resUser.LastName, firstname: resUser.FirstName, avatar: resUser.Photo200}
 | 
			
		||||
			b.usernamesMap[id] = u
 | 
			
		||||
		} else {
 | 
			
		||||
			result, _ := b.c.GroupsGetByID(api.Params{
 | 
			
		||||
				"group_id": id * -1,
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			resGroup := result[0]
 | 
			
		||||
			u = user{lastname: resGroup.Name, avatar: resGroup.Photo200}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return u
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) handleMessage(msg object.MessagesMessage, isFwd bool) {
 | 
			
		||||
	b.Log.Debug("ChatID: ", msg.PeerID)
 | 
			
		||||
	// fetch user info
 | 
			
		||||
	u := b.getUser(msg.FromID)
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		Text:     msg.Text,
 | 
			
		||||
		Username: u.firstname + " " + u.lastname,
 | 
			
		||||
		Avatar:   u.avatar,
 | 
			
		||||
		Channel:  strconv.Itoa(msg.PeerID),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		UserID:   strconv.Itoa(msg.FromID),
 | 
			
		||||
		ID:       strconv.Itoa(msg.ConversationMessageID),
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if msg.ReplyMessage != nil {
 | 
			
		||||
		ur := b.getUser(msg.ReplyMessage.FromID)
 | 
			
		||||
		rmsg.Text = "Re: " + ur.firstname + " " + ur.lastname + "\n" + rmsg.Text
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if isFwd {
 | 
			
		||||
		rmsg.Username = "Fwd: " + rmsg.Username
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(msg.Attachments) > 0 {
 | 
			
		||||
		urls, text := b.getFiles(msg.Attachments)
 | 
			
		||||
 | 
			
		||||
		if text != "" {
 | 
			
		||||
			rmsg.Text += "\n" + text
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// download
 | 
			
		||||
		b.downloadFiles(&rmsg, urls)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(msg.FwdMessages) > 0 {
 | 
			
		||||
		rmsg.Text += strconv.Itoa(len(msg.FwdMessages)) + " forwarded messages"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
 | 
			
		||||
	if len(msg.FwdMessages) > 0 {
 | 
			
		||||
		// recursive processing of forwarded messages
 | 
			
		||||
		for _, m := range msg.FwdMessages {
 | 
			
		||||
			m.PeerID = msg.PeerID
 | 
			
		||||
			b.handleMessage(m, true)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, string) {
 | 
			
		||||
	var attachments []string
 | 
			
		||||
	text := ""
 | 
			
		||||
 | 
			
		||||
	for _, f := range extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
 | 
			
		||||
		if fi.Comment != "" {
 | 
			
		||||
			text += fi.Comment + "\n"
 | 
			
		||||
		}
 | 
			
		||||
		a, err := b.uploadFile(fi, peerID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.WithError(err).Error("File upload error ", fi.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		attachments = append(attachments, a)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return strings.Join(attachments, ","), text
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) {
 | 
			
		||||
	r := bytes.NewReader(*file.Data)
 | 
			
		||||
 | 
			
		||||
	photoRE := regexp.MustCompile(".(jpg|jpe|png)$")
 | 
			
		||||
	if photoRE.MatchString(file.Name) {
 | 
			
		||||
		// BUG(VK): for community chat peerID=0
 | 
			
		||||
		p, err := b.c.UploadMessagesPhoto(0, r)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return photo + strconv.Itoa(p[0].OwnerID) + "_" + strconv.Itoa(p[0].ID), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var doctype string
 | 
			
		||||
	if strings.Contains(file.Name, ".ogg") {
 | 
			
		||||
		doctype = audioMessage
 | 
			
		||||
	} else {
 | 
			
		||||
		doctype = document
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	doc, err := b.c.UploadMessagesDoc(peerID, doctype, file.Name, "", r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch doc.Type {
 | 
			
		||||
	case audioMessage:
 | 
			
		||||
		return document + strconv.Itoa(doc.AudioMessage.OwnerID) + "_" + strconv.Itoa(doc.AudioMessage.ID), nil
 | 
			
		||||
	case document:
 | 
			
		||||
		return document + strconv.Itoa(doc.Doc.OwnerID) + "_" + strconv.Itoa(doc.Doc.ID), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) getFiles(attachments []object.MessagesMessageAttachment) ([]string, string) {
 | 
			
		||||
	var urls []string
 | 
			
		||||
	var text []string
 | 
			
		||||
 | 
			
		||||
	for _, a := range attachments {
 | 
			
		||||
		switch a.Type {
 | 
			
		||||
		case photo:
 | 
			
		||||
			var resolution float64 = 0
 | 
			
		||||
			url := a.Photo.Sizes[0].URL
 | 
			
		||||
			for _, size := range a.Photo.Sizes {
 | 
			
		||||
				r := size.Height * size.Width
 | 
			
		||||
				if resolution < r {
 | 
			
		||||
					resolution = r
 | 
			
		||||
					url = size.URL
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			urls = append(urls, url)
 | 
			
		||||
 | 
			
		||||
		case document:
 | 
			
		||||
			urls = append(urls, a.Doc.URL)
 | 
			
		||||
 | 
			
		||||
		case graffiti:
 | 
			
		||||
			urls = append(urls, a.Graffiti.URL)
 | 
			
		||||
 | 
			
		||||
		case audioMessage:
 | 
			
		||||
			urls = append(urls, a.AudioMessage.DocsDocPreviewAudioMessage.LinkOgg)
 | 
			
		||||
 | 
			
		||||
		case sticker:
 | 
			
		||||
			var resolution float64 = 0
 | 
			
		||||
			url := a.Sticker.Images[0].URL
 | 
			
		||||
			for _, size := range a.Sticker.Images {
 | 
			
		||||
				r := size.Height * size.Width
 | 
			
		||||
				if resolution < r {
 | 
			
		||||
					resolution = r
 | 
			
		||||
					url = size.URL
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			urls = append(urls, url+".png")
 | 
			
		||||
		case video:
 | 
			
		||||
			text = append(text, "https://vk.com/video"+strconv.Itoa(a.Video.OwnerID)+"_"+strconv.Itoa(a.Video.ID))
 | 
			
		||||
 | 
			
		||||
		case wall:
 | 
			
		||||
			text = append(text, "https://vk.com/wall"+strconv.Itoa(a.Wall.FromID)+"_"+strconv.Itoa(a.Wall.ID))
 | 
			
		||||
 | 
			
		||||
		default:
 | 
			
		||||
			text = append(text, "This attachment is not supported ("+a.Type+")")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return urls, strings.Join(text, "\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bvk) downloadFiles(rmsg *config.Message, urls []string) {
 | 
			
		||||
	for _, url := range urls {
 | 
			
		||||
		data, err := helper.DownloadFile(url)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			urlPart := strings.Split(url, "/")
 | 
			
		||||
			name := strings.Split(urlPart[len(urlPart)-1], "?")[0]
 | 
			
		||||
			helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +1,14 @@
 | 
			
		||||
// nolint:goconst
 | 
			
		||||
package bwhatsapp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/Rhymen/go-whatsapp"
 | 
			
		||||
	"github.com/jpillora/backoff"
 | 
			
		||||
 | 
			
		||||
	"github.com/matterbridge/go-whatsapp"
 | 
			
		||||
 | 
			
		||||
	whatsappExt "github.com/matterbridge/mautrix-whatsapp/whatsapp-ext"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@@ -23,56 +21,12 @@ Check:
 | 
			
		||||
 | 
			
		||||
// HandleError received from WhatsApp
 | 
			
		||||
func (b *Bwhatsapp) HandleError(err error) {
 | 
			
		||||
	// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
 | 
			
		||||
	// ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094
 | 
			
		||||
	if strings.Contains(err.Error(), "error processing data: received invalid data") ||
 | 
			
		||||
		strings.Contains(err.Error(), "invalid string with tag 174") {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch err.(type) {
 | 
			
		||||
	case *whatsapp.ErrConnectionClosed, *whatsapp.ErrConnectionFailed:
 | 
			
		||||
		b.reconnect(err)
 | 
			
		||||
	default:
 | 
			
		||||
		switch err {
 | 
			
		||||
		case whatsapp.ErrConnectionTimeout:
 | 
			
		||||
			b.reconnect(err)
 | 
			
		||||
		default:
 | 
			
		||||
			b.Log.Errorf("%v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) reconnect(err error) {
 | 
			
		||||
	bf := &backoff.Backoff{
 | 
			
		||||
		Min:    time.Second,
 | 
			
		||||
		Max:    5 * time.Minute,
 | 
			
		||||
		Jitter: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		d := bf.Duration()
 | 
			
		||||
 | 
			
		||||
		b.Log.Errorf("Connection failed, underlying error: %v", err)
 | 
			
		||||
		b.Log.Infof("Waiting %s...", d)
 | 
			
		||||
 | 
			
		||||
		time.Sleep(d)
 | 
			
		||||
 | 
			
		||||
		b.Log.Info("Reconnecting...")
 | 
			
		||||
 | 
			
		||||
		err := b.conn.Restore()
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			bf.Reset()
 | 
			
		||||
			b.startedAt = uint64(time.Now().Unix())
 | 
			
		||||
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Errorf("%v", err) // TODO implement proper handling? at least respond to different error types
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleTextMessage sent from WhatsApp, relay it to the brige
 | 
			
		||||
func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
 | 
			
		||||
	if message.Info.FromMe {
 | 
			
		||||
	if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// whatsapp sends last messages to show context , cut them
 | 
			
		||||
@@ -80,17 +34,17 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	groupJID := message.Info.RemoteJid
 | 
			
		||||
	senderJID := message.Info.SenderJid
 | 
			
		||||
	messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
 | 
			
		||||
	groupJid := message.Info.RemoteJid
 | 
			
		||||
 | 
			
		||||
	if len(senderJID) == 0 {
 | 
			
		||||
		if message.Info.Source != nil && message.Info.Source.Participant != nil {
 | 
			
		||||
			senderJID = *message.Info.Source.Participant
 | 
			
		||||
		}
 | 
			
		||||
	senderJid := message.Info.SenderJid
 | 
			
		||||
	if len(senderJid) == 0 {
 | 
			
		||||
		// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
 | 
			
		||||
		senderJid = *message.Info.Source.Participant
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// translate sender's JID to the nicest username we can get
 | 
			
		||||
	senderName := b.getSenderName(senderJID)
 | 
			
		||||
	// translate sender's Jid to the nicest username we can get
 | 
			
		||||
	senderName := b.getSenderName(senderJid)
 | 
			
		||||
	if senderName == "" {
 | 
			
		||||
		senderName = "Someone" // don't expose telephone number
 | 
			
		||||
	}
 | 
			
		||||
@@ -98,285 +52,53 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
 | 
			
		||||
	extText := message.Info.Source.Message.ExtendedTextMessage
 | 
			
		||||
	if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil {
 | 
			
		||||
		// handle user mentions
 | 
			
		||||
		for _, mentionedJID := range extText.ContextInfo.MentionedJid {
 | 
			
		||||
			numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
 | 
			
		||||
		for _, mentionedJid := range extText.ContextInfo.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(numberAndSuffix[0] + "@s.whatsapp.net")
 | 
			
		||||
			mention := b.getSenderNotify(numberAndSuffix[0] + whatsappExt.NewUserSuffix)
 | 
			
		||||
			if mention == "" {
 | 
			
		||||
				mention = "someone"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJid, b.Account)
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID,
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Text:     message.Text,
 | 
			
		||||
		Channel:  groupJID,
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		//	ParentID: TODO, // TODO handle thread replies  // map from Info.QuotedMessageID string
 | 
			
		||||
		ID: message.Info.Id,
 | 
			
		||||
	}
 | 
			
		||||
		UserID:    senderJid,
 | 
			
		||||
		Username:  senderName,
 | 
			
		||||
		Text:      message.Text,
 | 
			
		||||
		Timestamp: messageTime,
 | 
			
		||||
		Channel:   groupJid,
 | 
			
		||||
		Account:   b.Account,
 | 
			
		||||
		Protocol:  b.Protocol,
 | 
			
		||||
		Extra:     make(map[string][]interface{}),
 | 
			
		||||
		//		ParentID: TODO, // TODO handle thread replies  // map from Info.QuotedMessageID string
 | 
			
		||||
		//	Event     string    `json:"event"`
 | 
			
		||||
		//	Gateway   string  // will be added during message processing
 | 
			
		||||
		ID: message.Info.Id}
 | 
			
		||||
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJID]; exists {
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJid]; exists {
 | 
			
		||||
		rmsg.Avatar = avatarURL
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleImageMessage sent from WhatsApp, relay it to the brige
 | 
			
		||||
// nolint:funlen
 | 
			
		||||
func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
 | 
			
		||||
	if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	senderJID := message.Info.SenderJid
 | 
			
		||||
	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
 | 
			
		||||
		senderJID = *message.Info.Source.Participant
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	senderName := b.getSenderName(message.Info.SenderJid)
 | 
			
		||||
	if senderName == "" {
 | 
			
		||||
		senderName = "Someone" // don't expose telephone number
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID,
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Channel:  message.Info.RemoteJid,
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		ID:       message.Info.Id,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJID]; exists {
 | 
			
		||||
		rmsg.Avatar = avatarURL
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileExt, err := mime.ExtensionsByType(message.Type)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Mimetype detection error: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
 | 
			
		||||
	if fileExt[0] == ".jfif" {
 | 
			
		||||
		fileExt[0] = ".jpg"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463
 | 
			
		||||
	if fileExt[0] == ".jpe" {
 | 
			
		||||
		fileExt[0] = ".jpg"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Trying to download %s with type %s", filename, message.Type)
 | 
			
		||||
 | 
			
		||||
	data, err := message.Download()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Download image failed: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Move file to bridge storage
 | 
			
		||||
	helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleVideoMessage downloads video messages
 | 
			
		||||
func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
 | 
			
		||||
	if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	senderJID := message.Info.SenderJid
 | 
			
		||||
	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
 | 
			
		||||
		senderJID = *message.Info.Source.Participant
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	senderName := b.getSenderName(message.Info.SenderJid)
 | 
			
		||||
	if senderName == "" {
 | 
			
		||||
		senderName = "Someone" // don't expose telephone number
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID,
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Channel:  message.Info.RemoteJid,
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		ID:       message.Info.Id,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJID]; exists {
 | 
			
		||||
		rmsg.Avatar = avatarURL
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileExt, err := mime.ExtensionsByType(message.Type)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Mimetype detection error: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(fileExt) == 0 {
 | 
			
		||||
		fileExt = append(fileExt, ".mp4")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
 | 
			
		||||
 | 
			
		||||
	data, err := message.Download()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Download video failed: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Move file to bridge storage
 | 
			
		||||
	helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleAudioMessage downloads audio messages
 | 
			
		||||
func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) {
 | 
			
		||||
	if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	senderJID := message.Info.SenderJid
 | 
			
		||||
	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
 | 
			
		||||
		senderJID = *message.Info.Source.Participant
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	senderName := b.getSenderName(message.Info.SenderJid)
 | 
			
		||||
	if senderName == "" {
 | 
			
		||||
		senderName = "Someone" // don't expose telephone number
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID,
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Channel:  message.Info.RemoteJid,
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		ID:       message.Info.Id,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJID]; exists {
 | 
			
		||||
		rmsg.Avatar = avatarURL
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileExt, err := mime.ExtensionsByType(message.Type)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Mimetype detection error: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(fileExt) == 0 {
 | 
			
		||||
		fileExt = append(fileExt, ".ogg")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
 | 
			
		||||
 | 
			
		||||
	data, err := message.Download()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Download audio failed: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Move file to bridge storage
 | 
			
		||||
	helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleDocumentMessage downloads documents
 | 
			
		||||
func (b *Bwhatsapp) HandleDocumentMessage(message whatsapp.DocumentMessage) {
 | 
			
		||||
	if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	senderJID := message.Info.SenderJid
 | 
			
		||||
	if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
 | 
			
		||||
		senderJID = *message.Info.Source.Participant
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	senderName := b.getSenderName(message.Info.SenderJid)
 | 
			
		||||
	if senderName == "" {
 | 
			
		||||
		senderName = "Someone" // don't expose telephone number
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID,
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Channel:  message.Info.RemoteJid,
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		ID:       message.Info.Id,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if avatarURL, exists := b.userAvatars[senderJID]; exists {
 | 
			
		||||
		rmsg.Avatar = avatarURL
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileExt, err := mime.ExtensionsByType(message.Type)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Mimetype detection error: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filename := fmt.Sprintf("%v", message.FileName)
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, message.Type)
 | 
			
		||||
 | 
			
		||||
	data, err := message.Download()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Download document message failed: %s", err)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Move file to bridge storage
 | 
			
		||||
	helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General)
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
 | 
			
		||||
	b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
 | 
			
		||||
	b.Remote <- rmsg
 | 
			
		||||
}
 | 
			
		||||
//
 | 
			
		||||
//func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
 | 
			
		||||
//	fmt.Println(message) // TODO implement
 | 
			
		||||
//}
 | 
			
		||||
//
 | 
			
		||||
//func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
 | 
			
		||||
//	fmt.Println(message) // TODO implement
 | 
			
		||||
//}
 | 
			
		||||
//
 | 
			
		||||
//func (b *Bwhatsapp) HandleJsonMessage(message string) {
 | 
			
		||||
//	fmt.Println(message) // TODO implement
 | 
			
		||||
//}
 | 
			
		||||
// TODO HandleRawMessage
 | 
			
		||||
// TODO HandleAudioMessage
 | 
			
		||||
 
 | 
			
		||||
@@ -2,28 +2,17 @@ package bwhatsapp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
 | 
			
		||||
	"github.com/Rhymen/go-whatsapp"
 | 
			
		||||
	"github.com/matterbridge/go-whatsapp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ProfilePicInfo struct {
 | 
			
		||||
	URL    string `json:"eurl"`
 | 
			
		||||
	Tag    string `json:"tag"`
 | 
			
		||||
	Status int16  `json:"status"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func qrFromTerminal(invert bool) chan string {
 | 
			
		||||
	qr := make(chan string)
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		terminal := qrcodeTerminal.New()
 | 
			
		||||
 | 
			
		||||
		if invert {
 | 
			
		||||
			terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium)
 | 
			
		||||
		}
 | 
			
		||||
@@ -46,12 +35,13 @@ func (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return session, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	decoder := gob.NewDecoder(file)
 | 
			
		||||
 | 
			
		||||
	return session, decoder.Decode(&session)
 | 
			
		||||
	err = decoder.Decode(&session)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return session, err
 | 
			
		||||
	}
 | 
			
		||||
	return session, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
 | 
			
		||||
@@ -66,31 +56,11 @@ func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	encoder := gob.NewEncoder(file)
 | 
			
		||||
	err = encoder.Encode(session)
 | 
			
		||||
 | 
			
		||||
	return encoder.Encode(session)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) restoreSession() (*whatsapp.Session, error) {
 | 
			
		||||
	session, err := b.readSession()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Warn(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugln("Restoring WhatsApp session..")
 | 
			
		||||
 | 
			
		||||
	session, err = b.conn.RestoreWithSession(session)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// restore session connection timed out (I couldn't get over it without logging in again)
 | 
			
		||||
		return nil, errors.New("failed to restore session: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugln("Session restored successfully!")
 | 
			
		||||
 | 
			
		||||
	return &session, nil
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) getSenderName(senderJid string) string {
 | 
			
		||||
@@ -101,32 +71,8 @@ func (b *Bwhatsapp) getSenderName(senderJid string) string {
 | 
			
		||||
		// if user is not in phone contacts
 | 
			
		||||
		// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
 | 
			
		||||
		// users can change it in their WhatsApp settings -> profile -> click on Avatar
 | 
			
		||||
		if sender.Notify != "" {
 | 
			
		||||
			return sender.Notify
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if sender.Short != "" {
 | 
			
		||||
			return sender.Short
 | 
			
		||||
		}
 | 
			
		||||
		return sender.Notify
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// try to reload this contact
 | 
			
		||||
	if _, err := b.conn.Contacts(); err != nil {
 | 
			
		||||
		b.Log.Errorf("error on update of contacts: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if contact, exists := b.conn.Store.Contacts[senderJid]; exists {
 | 
			
		||||
		// Add it to the user map
 | 
			
		||||
		b.users[senderJid] = contact
 | 
			
		||||
 | 
			
		||||
		if contact.Name != "" {
 | 
			
		||||
			return contact.Name
 | 
			
		||||
		}
 | 
			
		||||
		// if user is not in phone contacts
 | 
			
		||||
		// same as above
 | 
			
		||||
		return contact.Notify
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -134,29 +80,5 @@ func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
 | 
			
		||||
	if sender, exists := b.users[senderJid]; exists {
 | 
			
		||||
		return sender.Notify
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
 | 
			
		||||
	data, err := b.conn.GetProfilePicThumb(jid)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to get avatar: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content := <-data
 | 
			
		||||
	info := &ProfilePicInfo{}
 | 
			
		||||
 | 
			
		||||
	err = json.Unmarshal([]byte(content), info)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return info, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isGroupJid(identifier string) bool {
 | 
			
		||||
	return strings.HasSuffix(identifier, "@g.us") ||
 | 
			
		||||
		strings.HasSuffix(identifier, "@temp") ||
 | 
			
		||||
		strings.HasSuffix(identifier, "@broadcast")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
package bwhatsapp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/Rhymen/go-whatsapp"
 | 
			
		||||
 | 
			
		||||
	"github.com/matterbridge/go-whatsapp"
 | 
			
		||||
 | 
			
		||||
	whatsappExt "github.com/matterbridge/mautrix-whatsapp/whatsapp-ext"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -28,8 +28,11 @@ const (
 | 
			
		||||
type Bwhatsapp struct {
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
 | 
			
		||||
	session   *whatsapp.Session
 | 
			
		||||
	conn      *whatsapp.Conn
 | 
			
		||||
	// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21
 | 
			
		||||
	session *whatsapp.Session
 | 
			
		||||
	conn    *whatsapp.Conn
 | 
			
		||||
	// https://github.com/tulir/mautrix-whatsapp/blob/master/whatsapp-ext/whatsapp.go
 | 
			
		||||
	connExt   *whatsappExt.ExtendedConn
 | 
			
		||||
	startedAt uint64
 | 
			
		||||
 | 
			
		||||
	users       map[string]whatsapp.Contact
 | 
			
		||||
@@ -39,12 +42,6 @@ type Bwhatsapp struct {
 | 
			
		||||
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
 | 
			
		||||
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")
 | 
			
		||||
	}
 | 
			
		||||
@@ -55,17 +52,21 @@ func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
		users:       make(map[string]whatsapp.Contact),
 | 
			
		||||
		userAvatars: make(map[string]string),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Connect to WhatsApp. Required implementation of the Bridger interface
 | 
			
		||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
 | 
			
		||||
func (b *Bwhatsapp) Connect() error {
 | 
			
		||||
	b.RLock() // TODO do we need locking for Whatsapp?
 | 
			
		||||
	defer b.RUnlock()
 | 
			
		||||
 | 
			
		||||
	number := b.GetString(cfgNumber)
 | 
			
		||||
	if number == "" {
 | 
			
		||||
		return errors.New("whatsapp's telephone number need to be configured")
 | 
			
		||||
		return errors.New("WhatsApp's telephone Number need to be configured")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// https://github.com/Rhymen/go-whatsapp#creating-a-connection
 | 
			
		||||
	b.Log.Debugln("Connecting to WhatsApp..")
 | 
			
		||||
	conn, err := whatsapp.NewConn(20 * time.Second)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -73,23 +74,42 @@ func (b *Bwhatsapp) Connect() error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.conn = conn
 | 
			
		||||
	b.connExt = whatsappExt.ExtendConn(b.conn)
 | 
			
		||||
	// TODO do we want to use it? b.connExt.SetClientName("Matterbridge WhatsApp bridge", "mb-wa")
 | 
			
		||||
 | 
			
		||||
	b.conn.AddHandler(b)
 | 
			
		||||
	b.Log.Debugln("WhatsApp connection successful")
 | 
			
		||||
 | 
			
		||||
	// load existing session in order to keep it between restarts
 | 
			
		||||
	b.session, err = b.restoreSession()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Warn(err.Error())
 | 
			
		||||
	if b.session == nil {
 | 
			
		||||
		var session whatsapp.Session
 | 
			
		||||
		session, err = b.readSession()
 | 
			
		||||
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			b.Log.Debugln("Restoring WhatsApp session..")
 | 
			
		||||
 | 
			
		||||
			// https://github.com/Rhymen/go-whatsapp#restore
 | 
			
		||||
			session, err = b.conn.RestoreSession(session)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				// TODO return or continue to normal login?
 | 
			
		||||
				// restore session connection timed out (I couldn't get over it without logging in again)
 | 
			
		||||
				return errors.New("failed to restore session: " + err.Error())
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			b.session = &session
 | 
			
		||||
			b.Log.Debugln("Session restored successfully!")
 | 
			
		||||
		} else {
 | 
			
		||||
			b.Log.Warn(err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// login to a new session
 | 
			
		||||
	if b.session == nil {
 | 
			
		||||
		if err = b.Login(); err != nil {
 | 
			
		||||
		err = b.Login()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.startedAt = uint64(time.Now().Unix())
 | 
			
		||||
 | 
			
		||||
	_, err = b.conn.Contacts()
 | 
			
		||||
@@ -97,13 +117,6 @@ func (b *Bwhatsapp) Connect() error {
 | 
			
		||||
		return fmt.Errorf("error on update of contacts: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
 | 
			
		||||
	for len(b.conn.Store.Contacts) == 0 {
 | 
			
		||||
		b.conn.Contacts() // nolint:errcheck
 | 
			
		||||
 | 
			
		||||
		<-time.After(1 * time.Second)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// map all the users
 | 
			
		||||
	for id, contact := range b.conn.Store.Contacts {
 | 
			
		||||
		if !isGroupJid(id) && id != "status@broadcast" {
 | 
			
		||||
@@ -117,16 +130,15 @@ func (b *Bwhatsapp) Connect() error {
 | 
			
		||||
		b.Log.Debug("Getting user avatars..")
 | 
			
		||||
 | 
			
		||||
		for jid := range b.users {
 | 
			
		||||
			info, err := b.GetProfilePicThumb(jid)
 | 
			
		||||
			info, err := b.connExt.GetProfilePicThumb(jid)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
 | 
			
		||||
 | 
			
		||||
			} else {
 | 
			
		||||
				b.Lock()
 | 
			
		||||
				// TODO any race conditions here?
 | 
			
		||||
				b.userAvatars[jid] = info.URL
 | 
			
		||||
				b.Unlock()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b.Log.Debug("Finished getting avatars..")
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
@@ -143,10 +155,8 @@ func (b *Bwhatsapp) Login() error {
 | 
			
		||||
	session, err := b.conn.Login(qrChan)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Warnln("Failed to log in:", err)
 | 
			
		||||
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.session = &session
 | 
			
		||||
 | 
			
		||||
	b.Log.Infof("Logged into session: %#v", session)
 | 
			
		||||
@@ -157,122 +167,74 @@ func (b *Bwhatsapp) Login() error {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO change connection strings to configured ones longClientName:"github.com/rhymen/go-whatsapp", shortClientName:"go-whatsapp"}" prefix=whatsapp
 | 
			
		||||
	// TODO get also a nice logo
 | 
			
		||||
 | 
			
		||||
	// TODO notification about unplugged and dead battery
 | 
			
		||||
	// conn.Info: Wid, Pushname, Connected, Battery, Plugged
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Disconnect is called while reconnecting to the bridge
 | 
			
		||||
// TODO 42wim Documentation would be helpful on when reconnects happen and what should be done in this function
 | 
			
		||||
// Required implementation of the Bridger interface
 | 
			
		||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
 | 
			
		||||
func (b *Bwhatsapp) Disconnect() error {
 | 
			
		||||
	// We could Logout, but that would close the session completely and would require a new QR code scan
 | 
			
		||||
	// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isGroupJid(identifier string) bool {
 | 
			
		||||
	return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
 | 
			
		||||
// Required implementation of the Bridger interface
 | 
			
		||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
 | 
			
		||||
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
	byJid := isGroupJid(channel.Name)
 | 
			
		||||
 | 
			
		||||
	// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
 | 
			
		||||
	for len(b.conn.Store.Contacts) == 0 {
 | 
			
		||||
		b.conn.Contacts() // nolint:errcheck
 | 
			
		||||
		<-time.After(1 * time.Second)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// verify if we are member of the given group
 | 
			
		||||
	if byJid {
 | 
			
		||||
		// channel.Name specifies static group jID, not the name
 | 
			
		||||
		if _, exists := b.conn.Store.Contacts[channel.Name]; !exists {
 | 
			
		||||
			return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// channel.Name specifies group name that might change, warn about it
 | 
			
		||||
	var jids []string
 | 
			
		||||
	for id, contact := range b.conn.Store.Contacts {
 | 
			
		||||
		if isGroupJid(id) && contact.Name == channel.Name {
 | 
			
		||||
			jids = append(jids, id)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch len(jids) {
 | 
			
		||||
	case 0:
 | 
			
		||||
		// didn't match any group - print out possibilites
 | 
			
		||||
	} else {
 | 
			
		||||
		// channel.Name specifies group name that might change, warn about it
 | 
			
		||||
		var jids []string
 | 
			
		||||
		for id, contact := range b.conn.Store.Contacts {
 | 
			
		||||
			if isGroupJid(id) {
 | 
			
		||||
				b.Log.Infof("%s %s", contact.Jid, contact.Name)
 | 
			
		||||
			if isGroupJid(id) && contact.Name == channel.Name {
 | 
			
		||||
				jids = append(jids, id)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
 | 
			
		||||
	case 1:
 | 
			
		||||
		return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		switch len(jids) {
 | 
			
		||||
		case 0:
 | 
			
		||||
			// didn't match any group - print out possibilites
 | 
			
		||||
			// TODO sort
 | 
			
		||||
			// copy b;
 | 
			
		||||
			//sort.Slice(people, func(i, j int) bool {
 | 
			
		||||
			//	return people[i].Age > people[j].Age
 | 
			
		||||
			//})
 | 
			
		||||
			for id, contact := range b.conn.Store.Contacts {
 | 
			
		||||
				if isGroupJid(id) {
 | 
			
		||||
					b.Log.Infof("%s %s", contact.Jid, contact.Name)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
 | 
			
		||||
 | 
			
		||||
// Post a document message from the bridge to WhatsApp
 | 
			
		||||
func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
 | 
			
		||||
	fi := msg.Extra["file"][0].(config.FileInfo)
 | 
			
		||||
		case 1:
 | 
			
		||||
			return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
 | 
			
		||||
 | 
			
		||||
	// Post document message
 | 
			
		||||
	message := whatsapp.DocumentMessage{
 | 
			
		||||
		Info: whatsapp.MessageInfo{
 | 
			
		||||
			RemoteJid: msg.Channel,
 | 
			
		||||
		},
 | 
			
		||||
		Title:    fi.Name,
 | 
			
		||||
		FileName: fi.Name,
 | 
			
		||||
		Type:     filetype,
 | 
			
		||||
		Content:  bytes.NewReader(*fi.Data),
 | 
			
		||||
		default:
 | 
			
		||||
			return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Sending %#v", msg)
 | 
			
		||||
 | 
			
		||||
	// create message ID
 | 
			
		||||
	// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
 | 
			
		||||
	idBytes := make([]byte, 10)
 | 
			
		||||
	if _, err := rand.Read(idBytes); err != nil {
 | 
			
		||||
		b.Log.Warn(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
 | 
			
		||||
	_, err := b.conn.Send(message)
 | 
			
		||||
 | 
			
		||||
	return message.Info.Id, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Post an image message from the bridge to WhatsApp
 | 
			
		||||
// Handle, for sure image/jpeg, image/png and image/gif MIME types
 | 
			
		||||
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
 | 
			
		||||
	fi := msg.Extra["file"][0].(config.FileInfo)
 | 
			
		||||
 | 
			
		||||
	// Post image message
 | 
			
		||||
	message := whatsapp.ImageMessage{
 | 
			
		||||
		Info: whatsapp.MessageInfo{
 | 
			
		||||
			RemoteJid: msg.Channel,
 | 
			
		||||
		},
 | 
			
		||||
		Type:    filetype,
 | 
			
		||||
		Caption: msg.Username + fi.Comment,
 | 
			
		||||
		Content: bytes.NewReader(*fi.Data),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Sending %#v", msg)
 | 
			
		||||
 | 
			
		||||
	// create message ID
 | 
			
		||||
	// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
 | 
			
		||||
	idBytes := make([]byte, 10)
 | 
			
		||||
	if _, err := rand.Read(idBytes); err != nil {
 | 
			
		||||
		b.Log.Warn(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
 | 
			
		||||
	_, err := b.conn.Send(message)
 | 
			
		||||
 | 
			
		||||
	return message.Info.Id, err
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Send a message from the bridge to WhatsApp
 | 
			
		||||
@@ -286,44 +248,36 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
 | 
			
		||||
		if msg.ID == "" {
 | 
			
		||||
			// No message ID in case action is executed on a message sent before the bridge was started
 | 
			
		||||
			// and then the bridge cache doesn't have this message ID mapped
 | 
			
		||||
 | 
			
		||||
			// TODO 42wim Doesn't the app get clogged with a ton of IDs after some time of running?
 | 
			
		||||
			// WhatsApp allows to set any ID so in that case we could use external IDs and don't do mapping
 | 
			
		||||
			// but external IDs are not set
 | 
			
		||||
			return "", nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err := b.conn.RevokeMessage(msg.Channel, msg.ID, true)
 | 
			
		||||
 | 
			
		||||
		return "", err
 | 
			
		||||
		// TODO delete message on WhatsApp https://github.com/Rhymen/go-whatsapp/issues/100
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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)"
 | 
			
		||||
		}
 | 
			
		||||
		msg.Text += " (edited)"
 | 
			
		||||
		// TODO handle edit as a message reply with updated text
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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)
 | 
			
		||||
		default:
 | 
			
		||||
			return b.PostDocumentMessage(msg, filetype)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	//// TODO Handle Upload a file
 | 
			
		||||
	//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)
 | 
			
		||||
	//	}
 | 
			
		||||
	//}
 | 
			
		||||
 | 
			
		||||
	// Post text message
 | 
			
		||||
	message := whatsapp.TextMessage{
 | 
			
		||||
	text := whatsapp.TextMessage{
 | 
			
		||||
		Info: whatsapp.MessageInfo{
 | 
			
		||||
			RemoteJid: msg.Channel, // which equals to group id
 | 
			
		||||
		},
 | 
			
		||||
@@ -332,7 +286,17 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Sending %#v", msg)
 | 
			
		||||
 | 
			
		||||
	return b.conn.Send(message)
 | 
			
		||||
	// create message ID
 | 
			
		||||
	// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
 | 
			
		||||
	bytes := make([]byte, 10)
 | 
			
		||||
	if _, err := rand.Read(bytes); err != nil {
 | 
			
		||||
		b.Log.Warn(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	text.Info.Id = strings.ToUpper(hex.EncodeToString(bytes))
 | 
			
		||||
 | 
			
		||||
	err := b.conn.Send(text)
 | 
			
		||||
 | 
			
		||||
	return text.Info.Id, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76
 | 
			
		||||
 
 | 
			
		||||
@@ -1,338 +0,0 @@
 | 
			
		||||
// +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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.Infof("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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
		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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		UserID:   senderJID.String(),
 | 
			
		||||
		Username: senderName,
 | 
			
		||||
		Text:     text,
 | 
			
		||||
		Channel:  channel.String(),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		Protocol: b.Protocol,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
		//      ParentID: TODO, // TODO handle thread replies  // map from Info.QuotedMessageID string
 | 
			
		||||
		ID: messageInfo.ID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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:       msg.Info.ID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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:       msg.Info.ID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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, 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:       msg.Info.ID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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:       msg.Info.ID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
@@ -1,124 +0,0 @@
 | 
			
		||||
//go:build whatsappmulti
 | 
			
		||||
// +build whatsappmulti
 | 
			
		||||
 | 
			
		||||
package bwhatsapp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"go.mau.fi/whatsmeow"
 | 
			
		||||
	"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) 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?_foreign_keys=on&_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
 | 
			
		||||
}
 | 
			
		||||
@@ -1,413 +0,0 @@
 | 
			
		||||
//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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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.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)
 | 
			
		||||
 | 
			
		||||
	groups, err := b.wc.GetJoinedGroups()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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 groups {
 | 
			
		||||
			if group.JID == gJID {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	foundGroups := []string{}
 | 
			
		||||
 | 
			
		||||
	for _, group := range groups {
 | 
			
		||||
		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 groups {
 | 
			
		||||
			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
 | 
			
		||||
 | 
			
		||||
	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,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	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.MediaImage)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var message proto.Message
 | 
			
		||||
 | 
			
		||||
	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,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Sending %#v as an image", msg)
 | 
			
		||||
 | 
			
		||||
	ID := whatsmeow.GenerateMessageID()
 | 
			
		||||
	_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
 | 
			
		||||
 | 
			
		||||
	return ID, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Post a video message from the bridge to WhatsApp
 | 
			
		||||
func (b *Bwhatsapp) PostVideoMessage(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.MediaVideo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var message proto.Message
 | 
			
		||||
 | 
			
		||||
	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,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Sending %#v as a video", msg)
 | 
			
		||||
 | 
			
		||||
	ID := whatsmeow.GenerateMessageID()
 | 
			
		||||
	_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
 | 
			
		||||
 | 
			
		||||
	return ID, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
 | 
			
		||||
	message.AudioMessage = &proto.AudioMessage{
 | 
			
		||||
		Mimetype:      &filetype,
 | 
			
		||||
		MediaKey:      resp.MediaKey,
 | 
			
		||||
		FileEncSha256: resp.FileEncSHA256,
 | 
			
		||||
		FileSha256:    resp.FileSHA256,
 | 
			
		||||
		FileLength:    goproto.Uint64(resp.FileLength),
 | 
			
		||||
		Url:           &resp.URL,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Sending %#v as audio", msg)
 | 
			
		||||
 | 
			
		||||
	ID := whatsmeow.GenerateMessageID()
 | 
			
		||||
	_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	text := msg.Username + msg.Text
 | 
			
		||||
 | 
			
		||||
	var message proto.Message
 | 
			
		||||
 | 
			
		||||
	message.Conversation = &text
 | 
			
		||||
 | 
			
		||||
	ID := whatsmeow.GenerateMessageID()
 | 
			
		||||
	_, err := b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
 | 
			
		||||
 | 
			
		||||
	return ID, err
 | 
			
		||||
}
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
package bxmpp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/matterbridge/go-xmpp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// handleDownloadAvatar downloads the avatar of userid from channel
 | 
			
		||||
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
 | 
			
		||||
// logs an error message if it fails
 | 
			
		||||
func (b *Bxmpp) handleDownloadAvatar(avatar xmpp.AvatarData) {
 | 
			
		||||
	rmsg := config.Message{
 | 
			
		||||
		Username: "system",
 | 
			
		||||
		Text:     "avatar",
 | 
			
		||||
		Channel:  b.parseChannel(avatar.From),
 | 
			
		||||
		Account:  b.Account,
 | 
			
		||||
		UserID:   avatar.From,
 | 
			
		||||
		Event:    config.EventAvatarDownload,
 | 
			
		||||
		Extra:    make(map[string][]interface{}),
 | 
			
		||||
	}
 | 
			
		||||
	if _, ok := b.avatarMap[avatar.From]; !ok {
 | 
			
		||||
		b.Log.Debugf("Avatar.From: %s", avatar.From)
 | 
			
		||||
 | 
			
		||||
		err := helper.HandleDownloadSize(b.Log, &rmsg, avatar.From+".png", int64(len(avatar.Data)), b.General)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Error(err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		helper.HandleDownloadData(b.Log, &rmsg, avatar.From+".png", rmsg.Text, "", &avatar.Data, b.General)
 | 
			
		||||
		b.Log.Debugf("Avatar download complete")
 | 
			
		||||
		b.Remote <- rmsg
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
package bxmpp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"regexp"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var pathRegex = regexp.MustCompile("[^a-zA-Z0-9]+")
 | 
			
		||||
 | 
			
		||||
// GetAvatar constructs a URL for a given user-avatar if it is available in the cache.
 | 
			
		||||
func getAvatar(av map[string]string, userid string, general *config.Protocol) string {
 | 
			
		||||
	if hash, ok := av[userid]; ok {
 | 
			
		||||
		// NOTE: This does not happen in bridge/helper/helper.go but messes up XMPP
 | 
			
		||||
		id := pathRegex.ReplaceAllString(userid, "_")
 | 
			
		||||
		return general.MediaServerDownload + "/" + hash + "/" + id + ".png"
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) cacheAvatar(msg *config.Message) string {
 | 
			
		||||
	fi := msg.Extra["file"][0].(config.FileInfo)
 | 
			
		||||
	/* if we have a sha we have successfully uploaded the file to the media server,
 | 
			
		||||
	so we can now cache the sha */
 | 
			
		||||
	if fi.SHA != "" {
 | 
			
		||||
		b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
 | 
			
		||||
		b.avatarMap[msg.UserID] = fi.SHA
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
@@ -1,14 +1,8 @@
 | 
			
		||||
package bxmpp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
@@ -20,36 +14,50 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Bxmpp struct {
 | 
			
		||||
	xc      *xmpp.Client
 | 
			
		||||
	xmppMap map[string]string
 | 
			
		||||
	*bridge.Config
 | 
			
		||||
 | 
			
		||||
	startTime time.Time
 | 
			
		||||
	xc        *xmpp.Client
 | 
			
		||||
	xmppMap   map[string]string
 | 
			
		||||
	connected bool
 | 
			
		||||
	sync.RWMutex
 | 
			
		||||
 | 
			
		||||
	avatarAvailability map[string]bool
 | 
			
		||||
	avatarMap          map[string]string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
	return &Bxmpp{
 | 
			
		||||
		Config:             cfg,
 | 
			
		||||
		xmppMap:            make(map[string]string),
 | 
			
		||||
		avatarAvailability: make(map[string]bool),
 | 
			
		||||
		avatarMap:          make(map[string]string),
 | 
			
		||||
	}
 | 
			
		||||
	b := &Bxmpp{Config: cfg}
 | 
			
		||||
	b.xmppMap = make(map[string]string)
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) Connect() error {
 | 
			
		||||
	var err error
 | 
			
		||||
	b.Log.Infof("Connecting %s", b.GetString("Server"))
 | 
			
		||||
	if err := b.createXMPP(); err != nil {
 | 
			
		||||
	b.xc, err = b.createXMPP()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Debugf("%#v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Info("Connection succeeded")
 | 
			
		||||
	go b.manageConnection()
 | 
			
		||||
	go func() {
 | 
			
		||||
		initial := true
 | 
			
		||||
		bf := &backoff.Backoff{
 | 
			
		||||
			Min:    time.Second,
 | 
			
		||||
			Max:    5 * time.Minute,
 | 
			
		||||
			Jitter: true,
 | 
			
		||||
		}
 | 
			
		||||
		for {
 | 
			
		||||
			if initial {
 | 
			
		||||
				b.handleXMPP()
 | 
			
		||||
				initial = false
 | 
			
		||||
			}
 | 
			
		||||
			d := bf.Duration()
 | 
			
		||||
			b.Log.Infof("Disconnected. Reconnecting in %s", d)
 | 
			
		||||
			time.Sleep(d)
 | 
			
		||||
			b.xc, err = b.createXMPP()
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels}
 | 
			
		||||
				b.handleXMPP()
 | 
			
		||||
				bf.Reset()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -68,190 +76,58 @@ func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) Send(msg config.Message) (string, error) {
 | 
			
		||||
	// should be fixed by using a cache instead of dropping
 | 
			
		||||
	if !b.Connected() {
 | 
			
		||||
		return "", fmt.Errorf("bridge %s not connected, dropping message %#v to bridge", b.Account, msg)
 | 
			
		||||
	}
 | 
			
		||||
	// ignore delete messages
 | 
			
		||||
	if msg.Event == config.EventMsgDelete {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.Log.Debugf("=> Receiving %#v", msg)
 | 
			
		||||
 | 
			
		||||
	if msg.Event == config.EventAvatarDownload {
 | 
			
		||||
		return b.cacheAvatar(&msg), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Make a action /me of the message, prepend the username with it.
 | 
			
		||||
	// https://xmpp.org/extensions/xep-0245.html
 | 
			
		||||
	if msg.Event == config.EventUserAction {
 | 
			
		||||
		msg.Username = "/me " + msg.Username
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support).
 | 
			
		||||
	var err error
 | 
			
		||||
	// Upload a file (in xmpp case send the upload URL because xmpp has no native upload support)
 | 
			
		||||
	if msg.Extra != nil {
 | 
			
		||||
		for _, rmsg := range helper.HandleExtra(&msg, b.General) {
 | 
			
		||||
			b.Log.Debugf("=> Sending attachement message %#v", rmsg)
 | 
			
		||||
			if b.GetString("WebhookURL") != "" {
 | 
			
		||||
				err = b.postSlackCompatibleWebhook(msg)
 | 
			
		||||
			} else {
 | 
			
		||||
				_, err = b.xc.Send(xmpp.Chat{
 | 
			
		||||
					Type:   "groupchat",
 | 
			
		||||
					Remote: rmsg.Channel + "@" + b.GetString("Muc"),
 | 
			
		||||
					Text:   rmsg.Username + rmsg.Text,
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				b.Log.WithError(err).Error("Unable to send message with share URL.")
 | 
			
		||||
			}
 | 
			
		||||
			b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: rmsg.Channel + "@" + b.GetString("Muc"), Text: rmsg.Username + rmsg.Text})
 | 
			
		||||
		}
 | 
			
		||||
		if len(msg.Extra["file"]) > 0 {
 | 
			
		||||
			return "", b.handleUploadFile(&msg)
 | 
			
		||||
			return b.handleUploadFile(&msg)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.GetString("WebhookURL") != "" {
 | 
			
		||||
		b.Log.Debugf("Sending message using Webhook")
 | 
			
		||||
		err := b.postSlackCompatibleWebhook(msg)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Log.Errorf("Failed to send message using webhook: %s", err)
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Post normal message.
 | 
			
		||||
	var msgReplaceID string
 | 
			
		||||
	msgID := xid.New().String()
 | 
			
		||||
	var msgreplaceid string
 | 
			
		||||
	msgid := xid.New().String()
 | 
			
		||||
	if msg.ID != "" {
 | 
			
		||||
		msgReplaceID = msg.ID
 | 
			
		||||
		msgid = msg.ID
 | 
			
		||||
		msgreplaceid = msg.ID
 | 
			
		||||
	}
 | 
			
		||||
	b.Log.Debugf("=> Sending message %#v", msg)
 | 
			
		||||
	if _, err := b.xc.Send(xmpp.Chat{
 | 
			
		||||
		Type:      "groupchat",
 | 
			
		||||
		Remote:    msg.Channel + "@" + b.GetString("Muc"),
 | 
			
		||||
		Text:      msg.Username + msg.Text,
 | 
			
		||||
		ID:        msgID,
 | 
			
		||||
		ReplaceID: msgReplaceID,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
	// Post normal message
 | 
			
		||||
	_, err := b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text, ID: msgid, ReplaceID: msgreplaceid})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return msgID, nil
 | 
			
		||||
	return msgid, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) postSlackCompatibleWebhook(msg config.Message) error {
 | 
			
		||||
	type XMPPWebhook struct {
 | 
			
		||||
		Username string `json:"username"`
 | 
			
		||||
		Text     string `json:"text"`
 | 
			
		||||
	}
 | 
			
		||||
	webhookBody, err := json.Marshal(XMPPWebhook{
 | 
			
		||||
		Username: msg.Username,
 | 
			
		||||
		Text:     msg.Text,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Failed to marshal webhook: %s", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.Post(b.GetString("WebhookURL")+"/"+url.QueryEscape(msg.Channel), "application/json", bytes.NewReader(webhookBody))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Log.Errorf("Failed to POST webhook: %s", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.Body.Close()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) createXMPP() error {
 | 
			
		||||
	var serverName string
 | 
			
		||||
	switch {
 | 
			
		||||
	case !b.GetBool("Anonymous"):
 | 
			
		||||
		if !strings.Contains(b.GetString("Jid"), "@") {
 | 
			
		||||
			return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid"))
 | 
			
		||||
		}
 | 
			
		||||
		serverName = strings.Split(b.GetString("Jid"), "@")[1]
 | 
			
		||||
	case !strings.Contains(b.GetString("Server"), ":"):
 | 
			
		||||
		serverName = strings.Split(b.GetString("Server"), ":")[0]
 | 
			
		||||
	default:
 | 
			
		||||
		serverName = b.GetString("Server")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tc := &tls.Config{
 | 
			
		||||
		ServerName:         serverName,
 | 
			
		||||
		InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	xmpp.DebugWriter = b.Log.Writer()
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
 | 
			
		||||
	tc := new(tls.Config)
 | 
			
		||||
	tc.InsecureSkipVerify = b.GetBool("SkipTLSVerify")
 | 
			
		||||
	tc.ServerName = strings.Split(b.GetString("Server"), ":")[0]
 | 
			
		||||
	options := xmpp.Options{
 | 
			
		||||
		Host:                         b.GetString("Server"),
 | 
			
		||||
		User:                         b.GetString("Jid"),
 | 
			
		||||
		Password:                     b.GetString("Password"),
 | 
			
		||||
		NoTLS:                        true,
 | 
			
		||||
		StartTLS:                     !b.GetBool("NoTLS"),
 | 
			
		||||
		StartTLS:                     true,
 | 
			
		||||
		TLSConfig:                    tc,
 | 
			
		||||
		Debug:                        b.GetBool("debug"),
 | 
			
		||||
		Logger:                       b.Log.Writer(),
 | 
			
		||||
		Session:                      true,
 | 
			
		||||
		Status:                       "",
 | 
			
		||||
		StatusMessage:                "",
 | 
			
		||||
		Resource:                     "",
 | 
			
		||||
		InsecureAllowUnencryptedAuth: b.GetBool("NoTLS"),
 | 
			
		||||
		InsecureAllowUnencryptedAuth: false,
 | 
			
		||||
	}
 | 
			
		||||
	var err error
 | 
			
		||||
	b.xc, err = options.NewClient()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) manageConnection() {
 | 
			
		||||
	b.setConnected(true)
 | 
			
		||||
	initial := true
 | 
			
		||||
	bf := &backoff.Backoff{
 | 
			
		||||
		Min:    time.Second,
 | 
			
		||||
		Max:    5 * time.Minute,
 | 
			
		||||
		Jitter: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Main connection loop. Each iteration corresponds to a successful
 | 
			
		||||
	// connection attempt and the subsequent handling of the connection.
 | 
			
		||||
	for {
 | 
			
		||||
		if initial {
 | 
			
		||||
			initial = false
 | 
			
		||||
		} else {
 | 
			
		||||
			b.Remote <- config.Message{
 | 
			
		||||
				Username: "system",
 | 
			
		||||
				Text:     "rejoin",
 | 
			
		||||
				Channel:  "",
 | 
			
		||||
				Account:  b.Account,
 | 
			
		||||
				Event:    config.EventRejoinChannels,
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := b.handleXMPP(); err != nil {
 | 
			
		||||
			b.Log.WithError(err).Error("Disconnected.")
 | 
			
		||||
			b.setConnected(false)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Reconnection loop using an exponential back-off strategy. We
 | 
			
		||||
		// only break out of the loop if we have successfully reconnected.
 | 
			
		||||
		for {
 | 
			
		||||
			d := bf.Duration()
 | 
			
		||||
			b.Log.Infof("Reconnecting in %s.", d)
 | 
			
		||||
			time.Sleep(d)
 | 
			
		||||
 | 
			
		||||
			b.Log.Infof("Reconnecting now.")
 | 
			
		||||
			if err := b.createXMPP(); err == nil {
 | 
			
		||||
				b.setConnected(true)
 | 
			
		||||
				bf.Reset()
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			b.Log.Warn("Failed to reconnect.")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return b.xc, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) xmppKeepAlive() chan bool {
 | 
			
		||||
@@ -263,7 +139,8 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
 | 
			
		||||
			select {
 | 
			
		||||
			case <-ticker.C:
 | 
			
		||||
				b.Log.Debugf("PING")
 | 
			
		||||
				if err := b.xc.PingC2S("", ""); err != nil {
 | 
			
		||||
				err := b.xc.PingC2S("", "")
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					b.Log.Debugf("PING failed %#v", err)
 | 
			
		||||
				}
 | 
			
		||||
			case <-done:
 | 
			
		||||
@@ -275,80 +152,53 @@ func (b *Bxmpp) xmppKeepAlive() chan bool {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) handleXMPP() error {
 | 
			
		||||
	var ok bool
 | 
			
		||||
	var msgid string
 | 
			
		||||
	b.startTime = time.Now()
 | 
			
		||||
 | 
			
		||||
	done := b.xmppKeepAlive()
 | 
			
		||||
	defer close(done)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		m, err := b.xc.Recv()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			// An error together with AvatarData is non-fatal
 | 
			
		||||
			switch m.(type) {
 | 
			
		||||
			case xmpp.AvatarData:
 | 
			
		||||
				continue
 | 
			
		||||
			default:
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch v := m.(type) {
 | 
			
		||||
		case xmpp.Chat:
 | 
			
		||||
			if v.Type == "groupchat" {
 | 
			
		||||
				b.Log.Debugf("== Receiving %#v", v)
 | 
			
		||||
 | 
			
		||||
				// Skip invalid messages.
 | 
			
		||||
				event := ""
 | 
			
		||||
				// skip invalid messages
 | 
			
		||||
				if b.skipMessage(v) {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				var event string
 | 
			
		||||
				if strings.Contains(v.Text, "has set the subject to:") {
 | 
			
		||||
					event = config.EventTopicChange
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				available, sok := b.avatarAvailability[v.Remote]
 | 
			
		||||
				avatar := ""
 | 
			
		||||
				if !sok {
 | 
			
		||||
					b.Log.Debugf("Requesting avatar data")
 | 
			
		||||
					b.avatarAvailability[v.Remote] = false
 | 
			
		||||
					b.xc.AvatarRequestData(v.Remote)
 | 
			
		||||
				} else if available {
 | 
			
		||||
					avatar = getAvatar(b.avatarMap, v.Remote, b.General)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				msgID := v.ID
 | 
			
		||||
				msgid = v.ID
 | 
			
		||||
				if v.ReplaceID != "" {
 | 
			
		||||
					msgID = v.ReplaceID
 | 
			
		||||
					msgid = v.ReplaceID
 | 
			
		||||
				}
 | 
			
		||||
				rmsg := config.Message{
 | 
			
		||||
					Username: b.parseNick(v.Remote),
 | 
			
		||||
					Text:     v.Text,
 | 
			
		||||
					Channel:  b.parseChannel(v.Remote),
 | 
			
		||||
					Account:  b.Account,
 | 
			
		||||
					Avatar:   avatar,
 | 
			
		||||
					UserID:   v.Remote,
 | 
			
		||||
					ID:       msgID,
 | 
			
		||||
					ID:       msgid,
 | 
			
		||||
					Event:    event,
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Check if we have an action event.
 | 
			
		||||
				var ok bool
 | 
			
		||||
				// check if we have an action event
 | 
			
		||||
				rmsg.Text, ok = b.replaceAction(rmsg.Text)
 | 
			
		||||
				if ok {
 | 
			
		||||
					rmsg.Event = config.EventUserAction
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
 | 
			
		||||
				b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
				b.Remote <- rmsg
 | 
			
		||||
			}
 | 
			
		||||
		case xmpp.AvatarData:
 | 
			
		||||
			b.handleDownloadAvatar(v)
 | 
			
		||||
			b.avatarAvailability[v.From] = true
 | 
			
		||||
			b.Log.Debugf("Avatar for %s is now available", v.From)
 | 
			
		||||
		case xmpp.Presence:
 | 
			
		||||
			// Do nothing.
 | 
			
		||||
			// do nothing
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -361,46 +211,35 @@ func (b *Bxmpp) replaceAction(text string) (string, bool) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleUploadFile handles native upload of files
 | 
			
		||||
func (b *Bxmpp) handleUploadFile(msg *config.Message) error {
 | 
			
		||||
	var urlDesc string
 | 
			
		||||
func (b *Bxmpp) handleUploadFile(msg *config.Message) (string, error) {
 | 
			
		||||
	var urldesc = ""
 | 
			
		||||
 | 
			
		||||
	for _, file := range msg.Extra["file"] {
 | 
			
		||||
		fileInfo := file.(config.FileInfo)
 | 
			
		||||
		if fileInfo.Comment != "" {
 | 
			
		||||
			msg.Text += fileInfo.Comment + ": "
 | 
			
		||||
	for _, f := range msg.Extra["file"] {
 | 
			
		||||
		fi := f.(config.FileInfo)
 | 
			
		||||
		if fi.Comment != "" {
 | 
			
		||||
			msg.Text += fi.Comment + ": "
 | 
			
		||||
		}
 | 
			
		||||
		if fileInfo.URL != "" {
 | 
			
		||||
			msg.Text = fileInfo.URL
 | 
			
		||||
			if fileInfo.Comment != "" {
 | 
			
		||||
				msg.Text = fileInfo.Comment + ": " + fileInfo.URL
 | 
			
		||||
				urlDesc = fileInfo.Comment
 | 
			
		||||
		if fi.URL != "" {
 | 
			
		||||
			msg.Text = fi.URL
 | 
			
		||||
			if fi.Comment != "" {
 | 
			
		||||
				msg.Text = fi.Comment + ": " + fi.URL
 | 
			
		||||
				urldesc = fi.Comment
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if _, err := b.xc.Send(xmpp.Chat{
 | 
			
		||||
			Type:   "groupchat",
 | 
			
		||||
			Remote: msg.Channel + "@" + b.GetString("Muc"),
 | 
			
		||||
			Text:   msg.Username + msg.Text,
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		_, err := b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if fileInfo.URL != "" {
 | 
			
		||||
			if _, err := b.xc.SendOOB(xmpp.Chat{
 | 
			
		||||
				Type:    "groupchat",
 | 
			
		||||
				Remote:  msg.Channel + "@" + b.GetString("Muc"),
 | 
			
		||||
				Ooburl:  fileInfo.URL,
 | 
			
		||||
				Oobdesc: urlDesc,
 | 
			
		||||
			}); err != nil {
 | 
			
		||||
				b.Log.WithError(err).Warn("Failed to send share URL.")
 | 
			
		||||
			}
 | 
			
		||||
		if fi.URL != "" {
 | 
			
		||||
			b.xc.SendOOB(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Ooburl: fi.URL, Oobdesc: urldesc})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) parseNick(remote string) string {
 | 
			
		||||
	s := strings.Split(remote, "@")
 | 
			
		||||
	if len(s) > 1 {
 | 
			
		||||
	if len(s) > 0 {
 | 
			
		||||
		s = strings.Split(s[1], "/")
 | 
			
		||||
		if len(s) == 2 {
 | 
			
		||||
			return s[1] // nick
 | 
			
		||||
@@ -439,23 +278,7 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ignore messages posted by our webhook
 | 
			
		||||
	if b.GetString("WebhookURL") != "" && strings.Contains(message.ID, "webhookbot") {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// skip delayed messages
 | 
			
		||||
	return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) setConnected(state bool) {
 | 
			
		||||
	b.Lock()
 | 
			
		||||
	b.connected = state
 | 
			
		||||
	defer b.Unlock()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bxmpp) Connected() bool {
 | 
			
		||||
	b.RLock()
 | 
			
		||||
	defer b.RUnlock()
 | 
			
		||||
	return b.connected
 | 
			
		||||
	t := time.Time{}
 | 
			
		||||
	return message.Stamp != t
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ package bzulip
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -12,7 +11,6 @@ import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/config"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/helper"
 | 
			
		||||
	"github.com/42wim/matterbridge/version"
 | 
			
		||||
	gzb "github.com/matterbridge/gozulipbot"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +27,7 @@ func New(cfg *bridge.Config) bridge.Bridger {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bzulip) Connect() error {
 | 
			
		||||
	bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login"), UserAgent: fmt.Sprintf("matterbridge/%s", version.Release)}
 | 
			
		||||
	bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login")}
 | 
			
		||||
	bot.Init()
 | 
			
		||||
	q, err := bot.RegisterAll()
 | 
			
		||||
	b.q = q
 | 
			
		||||
@@ -127,7 +125,6 @@ func (b *Bzulip) handleQueue() error {
 | 
			
		||||
			b.Log.Debug("heartbeat received.")
 | 
			
		||||
		default:
 | 
			
		||||
			b.Log.Debugf("receiving error: %#v", err)
 | 
			
		||||
			time.Sleep(time.Second * 10)
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
@@ -138,25 +135,19 @@ func (b *Bzulip) handleQueue() error {
 | 
			
		||||
			if m.SenderEmail == b.GetString("login") {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			avatarURL := m.AvatarURL
 | 
			
		||||
			if !strings.HasPrefix(avatarURL, "http") {
 | 
			
		||||
				avatarURL = b.GetString("server") + avatarURL
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			rmsg := config.Message{
 | 
			
		||||
				Username: m.SenderFullName,
 | 
			
		||||
				Text:     m.Content,
 | 
			
		||||
				Channel:  b.getChannel(m.StreamID) + "/topic:" + m.Subject,
 | 
			
		||||
				Account:  b.Account,
 | 
			
		||||
				UserID:   strconv.Itoa(m.SenderID),
 | 
			
		||||
				Avatar:   avatarURL,
 | 
			
		||||
				Avatar:   m.AvatarURL,
 | 
			
		||||
			}
 | 
			
		||||
			b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
 | 
			
		||||
			b.Log.Debugf("<= Message is %#v", rmsg)
 | 
			
		||||
			b.Remote <- rmsg
 | 
			
		||||
			b.q.LastEventID = m.ID
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		time.Sleep(time.Second * 3)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1842
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										1842
									
								
								changelog.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										30
									
								
								ci/bintray.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								ci/bintray.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
set -u -e -x -o pipefail
 | 
			
		||||
 | 
			
		||||
go version | grep go1.12 || exit
 | 
			
		||||
 | 
			
		||||
VERSION=$(git describe --tags)
 | 
			
		||||
mkdir ci/binaries
 | 
			
		||||
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe
 | 
			
		||||
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-amd64
 | 
			
		||||
GOOS=linux GOARCH=arm go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-arm
 | 
			
		||||
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-darwin-amd64
 | 
			
		||||
cd ci
 | 
			
		||||
cat > deploy.json <<EOF
 | 
			
		||||
{
 | 
			
		||||
    "package": {
 | 
			
		||||
        "name": "Matterbridge",
 | 
			
		||||
        "repo": "nightly",
 | 
			
		||||
        "subject": "42wim"
 | 
			
		||||
    },
 | 
			
		||||
    "version": {
 | 
			
		||||
        "name": "$VERSION"
 | 
			
		||||
    },
 | 
			
		||||
    "files":
 | 
			
		||||
        [
 | 
			
		||||
        {"includePattern": "ci/binaries/(.*)", "uploadPattern":"\$1"}
 | 
			
		||||
        ],
 | 
			
		||||
    "publish": true
 | 
			
		||||
}
 | 
			
		||||
EOF
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								ci/lint.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										17
									
								
								ci/lint.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
set -u -e -x -o pipefail
 | 
			
		||||
 | 
			
		||||
if [[ -n "${GOLANGCI_VERSION-}" ]]; then
 | 
			
		||||
  # Retrieve the golangci-lint linter binary.
 | 
			
		||||
  curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Run the linter.
 | 
			
		||||
golangci-lint run
 | 
			
		||||
 | 
			
		||||
if [[ "${GO111MODULE-off}" == "on" ]]; then
 | 
			
		||||
  # If Go modules are active then check that dependencies are correctly maintained.
 | 
			
		||||
  go mod tidy
 | 
			
		||||
  go mod vendor
 | 
			
		||||
  git diff --exit-code --quiet || (echo "Please run 'go mod tidy' to clean up the 'go.mod' and 'go.sum' files."; false)
 | 
			
		||||
fi
 | 
			
		||||
							
								
								
									
										17
									
								
								ci/test.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										17
									
								
								ci/test.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
set -u -e -x -o pipefail
 | 
			
		||||
 | 
			
		||||
if [[ -n "${REPORT_COVERAGE+cover}" ]]; then
 | 
			
		||||
  # Retrieve and prepare CodeClimate's test coverage reporter.
 | 
			
		||||
  curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
 | 
			
		||||
  chmod +x ./cc-test-reporter
 | 
			
		||||
  ./cc-test-reporter before-build
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Run all the tests with the race detector and generate coverage.
 | 
			
		||||
go test -v -race -coverprofile c.out ./...
 | 
			
		||||
 | 
			
		||||
if [[ -n "${REPORT_COVERAGE+cover}" && "${TRAVIS_SECURE_ENV_VARS}" == "true" ]]; then
 | 
			
		||||
  # Upload test coverage to CodeClimate.
 | 
			
		||||
  ./cc-test-reporter after-build
 | 
			
		||||
fi
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
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()
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
#!/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
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
text := import("text")
 | 
			
		||||
 | 
			
		||||
// if we're not sending to a discord bridge,
 | 
			
		||||
// then convert custom emoji tags into url's
 | 
			
		||||
if (inProtocol == "discord" && outProtocol != "discord") {
 | 
			
		||||
    rePNG := text.re_compile(`<:.*?:([0-9]+)>`)
 | 
			
		||||
    msgText=rePNG.replace(msgText,"https://cdn.discordapp.com/emojis/$1.png")
 | 
			
		||||
    reGIF := text.re_compile(`<a:.*?:([0-9]+)>`)
 | 
			
		||||
    msgText=reGIF.replace(msgText,"https://cdn.discordapp.com/emojis/$1.gif")
 | 
			
		||||
}
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
// See https://github.com/42wim/matterbridge/issues/881
 | 
			
		||||
// Generates a colored nick for each msgUsername, with example to filter specific codes 
 | 
			
		||||
 | 
			
		||||
text := import("text")
 | 
			
		||||
fmt := import("fmt")
 | 
			
		||||
if outProtocol == "irc" {
 | 
			
		||||
    // generate a color for a nick, make sure it isn't 0 or 15
 | 
			
		||||
    colorCode := len(msgUsername)+bytes(msgUsername)[0]%14 + 2
 | 
			
		||||
    // example if we want to use colorCode 3 when we have calculated colorcode 14
 | 
			
		||||
    if colorCode == 14 {
 | 
			
		||||
        colorCode = 3
 | 
			
		||||
    }
 | 
			
		||||
    msgUsername=fmt.sprintf("\x03%02d%s\x0F", colorCode, msgUsername)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
// See https://github.com/42wim/matterbridge/issues/798
 | 
			
		||||
 | 
			
		||||
// if we're not sending to an irc bridge we strip the IRC colors
 | 
			
		||||
if outProtocol != "irc" {
 | 
			
		||||
    re := text.re_compile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
 | 
			
		||||
    msgText=re.replace(msgText,"")
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
This script will return the nick except with multi-character usernames
 | 
			
		||||
containing a zero-width space between the first and second character letter.
 | 
			
		||||
 | 
			
		||||
Single character usernames will be left untouched.
 | 
			
		||||
 | 
			
		||||
This is useful to prevent remote users from nickalerting
 | 
			
		||||
IRC users of the same name when the remote user speaks.
 | 
			
		||||
 | 
			
		||||
This result can be used in {TENGO} in RemoteNickFormat.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
result = nick
 | 
			
		||||
if len(nick) > 1 {
 | 
			
		||||
    result = string(nick[0]) + "" + nick[1:]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
This script will return the current time in kitchen format if the protocol (of the remote bridge) isn't irc
 | 
			
		||||
See https://github.com/d5/tengo/blob/master/docs/stdlib-times.md
 | 
			
		||||
This result can be used in {TENGO} in RemoteNickFormat
 | 
			
		||||
*/
 | 
			
		||||
times := import("times")
 | 
			
		||||
if protocol != "irc" {
 | 
			
		||||
   result=times.time_format(times.now(),times.format_kitchen)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
FROM alpine:edge as certs
 | 
			
		||||
RUN apk --update add ca-certificates
 | 
			
		||||
ARG VERSION=1.22.3
 | 
			
		||||
ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-${VERSION}-linux-arm64 /bin/matterbridge
 | 
			
		||||
RUN chmod +x /bin/matterbridge
 | 
			
		||||
 | 
			
		||||
FROM scratch
 | 
			
		||||
ARG VERSION=1.12.3
 | 
			
		||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
 | 
			
		||||
COPY --from=certs /bin/matterbridge /bin/matterbridge
 | 
			
		||||
ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-linux-arm /bin/matterbridge
 | 
			
		||||
RUN chmod +x /bin/matterbridge
 | 
			
		||||
ENTRYPOINT ["/bin/matterbridge"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !noapi
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["api"] = api.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
// +build !nodiscord
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bdiscord "github.com/42wim/matterbridge/bridge/discord"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["discord"] = bdiscord.New
 | 
			
		||||
	UserTypingSupport["discord"] = struct{}{}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !nogitter
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bgitter "github.com/42wim/matterbridge/bridge/gitter"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["gitter"] = bgitter.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
//go:build !noharmony
 | 
			
		||||
// +build !noharmony
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bharmony "github.com/42wim/matterbridge/bridge/harmony"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["harmony"] = bharmony.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !noirc
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	birc "github.com/42wim/matterbridge/bridge/irc"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["irc"] = birc.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !nokeybase
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bkeybase "github.com/42wim/matterbridge/bridge/keybase"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["keybase"] = bkeybase.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !nomatrix
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bmatrix "github.com/42wim/matterbridge/bridge/matrix"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["matrix"] = bmatrix.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !nomattermost
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bmattermost "github.com/42wim/matterbridge/bridge/mattermost"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["mattermost"] = bmattermost.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !nomsteams
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bmsteams "github.com/42wim/matterbridge/bridge/msteams"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["msteams"] = bmsteams.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !nomumble
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bmumble "github.com/42wim/matterbridge/bridge/mumble"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["mumble"] = bmumble.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !nonctalk
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	btalk "github.com/42wim/matterbridge/bridge/nctalk"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["nctalk"] = btalk.New
 | 
			
		||||
}
 | 
			
		||||
@@ -2,9 +2,36 @@ package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/api"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/discord"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/gitter"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/irc"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/matrix"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/mattermost"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/rocketchat"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/slack"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/sshchat"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/steam"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/telegram"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/whatsapp"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/xmpp"
 | 
			
		||||
	"github.com/42wim/matterbridge/bridge/zulip"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	FullMap           = map[string]bridge.Factory{}
 | 
			
		||||
	UserTypingSupport = map[string]struct{}{}
 | 
			
		||||
)
 | 
			
		||||
var FullMap = map[string]bridge.Factory{
 | 
			
		||||
	"api":          api.New,
 | 
			
		||||
	"discord":      bdiscord.New,
 | 
			
		||||
	"gitter":       bgitter.New,
 | 
			
		||||
	"irc":          birc.New,
 | 
			
		||||
	"mattermost":   bmattermost.New,
 | 
			
		||||
	"matrix":       bmatrix.New,
 | 
			
		||||
	"rocketchat":   brocketchat.New,
 | 
			
		||||
	"slack-legacy": bslack.NewLegacy,
 | 
			
		||||
	"slack":        bslack.New,
 | 
			
		||||
	"sshchat":      bsshchat.New,
 | 
			
		||||
	"steam":        bsteam.New,
 | 
			
		||||
	"telegram":     btelegram.New,
 | 
			
		||||
	"whatsapp":     bwhatsapp.New,
 | 
			
		||||
	"xmpp":         bxmpp.New,
 | 
			
		||||
	"zulip":        bzulip.New,
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !norocketchat
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	brocketchat "github.com/42wim/matterbridge/bridge/rocketchat"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["rocketchat"] = brocketchat.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
// +build !noslack
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bslack "github.com/42wim/matterbridge/bridge/slack"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["slack-legacy"] = bslack.NewLegacy
 | 
			
		||||
	FullMap["slack"] = bslack.New
 | 
			
		||||
	UserTypingSupport["slack"] = struct{}{}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !nosshchat
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bsshchat "github.com/42wim/matterbridge/bridge/sshchat"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["sshchat"] = bsshchat.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !nosteam
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	bsteam "github.com/42wim/matterbridge/bridge/steam"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["steam"] = bsteam.New
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
// +build !notelegram
 | 
			
		||||
 | 
			
		||||
package bridgemap
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	btelegram "github.com/42wim/matterbridge/bridge/telegram"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	FullMap["telegram"] = btelegram.New
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user