Compare commits
327 Commits
addtengodo
...
updatexmpp
Author | SHA1 | Date | |
---|---|---|---|
bab3681ac2 | |||
b74b884793 | |||
![]() |
c4157a4d5b | ||
![]() |
d16645c952 | ||
![]() |
65d78e38af | ||
![]() |
996e4a7fcf | ||
![]() |
a89267943c | ||
![]() |
815d8b804f | ||
![]() |
6edd5de3b7 | ||
![]() |
2e9db32a83 | ||
![]() |
4bf1c0450c | ||
![]() |
0bb521512a | ||
![]() |
13fd5c5d5b | ||
![]() |
d055b4530e | ||
![]() |
6b528ffa4f | ||
![]() |
fa147c076f | ||
![]() |
b2df32bc81 | ||
![]() |
733f4c71b8 | ||
![]() |
70e8c6e9d3 | ||
![]() |
2f33fe86f5 | ||
![]() |
56e7bd01ca | ||
![]() |
9459495484 | ||
![]() |
89b0d362d2 | ||
![]() |
574f25337d | ||
![]() |
5bbe422161 | ||
![]() |
6500714a93 | ||
![]() |
5feafcddba | ||
![]() |
3e20a3d180 | ||
![]() |
60639b6e50 | ||
![]() |
839f384e45 | ||
![]() |
d42277979a | ||
![]() |
24cf007a74 | ||
![]() |
601f48a50e | ||
![]() |
c2b8e298d8 | ||
![]() |
0917e17383 | ||
![]() |
8587fa8585 | ||
![]() |
f345eeae55 | ||
![]() |
89e2dbac15 | ||
![]() |
356ada872c | ||
![]() |
a3deb48726 | ||
![]() |
544cd5cd9e | ||
![]() |
53b63adc71 | ||
![]() |
2c349c50c7 | ||
![]() |
08779c2909 | ||
![]() |
d5f9cdf912 | ||
![]() |
3a4bdd7c56 | ||
![]() |
2b236f3ff7 | ||
![]() |
768fb791c5 | ||
![]() |
d00dcf3f58 | ||
![]() |
d5980303e5 | ||
![]() |
f436ae7b31 | ||
![]() |
a9ccc18d73 | ||
![]() |
ac681687f8 | ||
![]() |
57ce19150f | ||
![]() |
24f6747516 | ||
![]() |
6dafebc7cc | ||
![]() |
d23f2560d5 | ||
![]() |
880586bac4 | ||
![]() |
eac2a8c8dc | ||
![]() |
4cf313c4c6 | ||
![]() |
91353d0a4d | ||
![]() |
0a6d64ab48 | ||
![]() |
4fd0a76727 | ||
![]() |
6da9d567dc | ||
![]() |
6d5a3dff22 | ||
![]() |
3ad5deaff1 | ||
![]() |
9bbdf70e69 | ||
![]() |
0c83946983 | ||
![]() |
fda05f2262 | ||
![]() |
7abf1a5884 | ||
![]() |
365acc36ea | ||
![]() |
0482cd191d | ||
![]() |
6a3fc71397 | ||
![]() |
3c4192ebf6 | ||
![]() |
e450e1c447 | ||
![]() |
20f841c513 | ||
![]() |
d07a3e09c9 | ||
![]() |
4649876956 | ||
![]() |
5604d140e3 | ||
![]() |
8751fb4bb1 | ||
![]() |
3819062574 | ||
![]() |
051e6e76e9 | ||
![]() |
1e55dd47f2 | ||
![]() |
700b95546b | ||
![]() |
2fa96ec0ed | ||
![]() |
81e6f75aa4 | ||
![]() |
888c8b9a84 | ||
![]() |
e775a8a22e | ||
![]() |
99fbd9cae6 | ||
![]() |
67adad3e08 | ||
![]() |
2fca3c7563 | ||
![]() |
c3573f1a46 | ||
![]() |
ee932a9f8e | ||
![]() |
ce18c948e6 | ||
![]() |
7bc93c5506 | ||
![]() |
d7cad3b404 | ||
![]() |
7740a362c9 | ||
![]() |
281ef53e7d | ||
![]() |
f044b948e2 | ||
![]() |
32474a5f4d | ||
![]() |
26596acf80 | ||
![]() |
e63870a631 | ||
![]() |
ce782ff6fb | ||
![]() |
c6716e030c | ||
![]() |
4ab72acec6 | ||
![]() |
30aae8e257 | ||
![]() |
d7b7ff7bb4 | ||
![]() |
6fe0cff342 | ||
![]() |
5f75f9886d | ||
![]() |
5d9604cd15 | ||
![]() |
cc36ebf1c9 | ||
![]() |
e6adecfd81 | ||
![]() |
5c8f224e3b | ||
![]() |
952221d3b9 | ||
![]() |
496d5b4ec7 | ||
![]() |
2623a412c4 | ||
![]() |
d64eed49bc | ||
![]() |
fffa29c2f3 | ||
![]() |
4da1444ffc | ||
![]() |
21c4e56d16 | ||
![]() |
5356b3856a | ||
![]() |
320c996a21 | ||
![]() |
69c74be7bb | ||
![]() |
aefa70891c | ||
![]() |
1b9877fda4 | ||
![]() |
0205a67309 | ||
![]() |
e3cafeaf92 | ||
![]() |
e7b193788a | ||
![]() |
17da95b094 | ||
![]() |
c5e49eec96 | ||
![]() |
24bc0f127b | ||
![]() |
f0f801402d | ||
![]() |
663850a2b8 | ||
![]() |
c51753cab1 | ||
![]() |
b3be2e208c | ||
![]() |
c30e90ff3f | ||
![]() |
e4c0ca0f48 | ||
![]() |
9c203327c0 | ||
![]() |
ccb5b1d075 | ||
![]() |
0dbbd0414c | ||
![]() |
e7b3ebf98a | ||
![]() |
5bc18fb780 | ||
![]() |
df30366072 | ||
![]() |
65c7ac80b5 | ||
![]() |
dd3fb32ec7 | ||
![]() |
2a3f475ff5 | ||
![]() |
7288f71201 | ||
![]() |
9c43eff753 | ||
![]() |
c8d7fdeedc | ||
![]() |
c211152e23 | ||
![]() |
ab75d5097e | ||
![]() |
c3644c8d3b | ||
![]() |
6438a3dba3 | ||
![]() |
4b226a6a63 | ||
![]() |
4801850013 | ||
![]() |
6a7412bf2b | ||
![]() |
5a1fd7dadd | ||
![]() |
ac06a26809 | ||
![]() |
61d56f26f8 | ||
![]() |
6aa05b3981 | ||
![]() |
aad60c882e | ||
![]() |
fecca57507 | ||
![]() |
2bcad846c0 | ||
![]() |
15ad0165fc | ||
![]() |
2e8ab11978 | ||
![]() |
9a8ce9b17e | ||
![]() |
16ab4c6fed | ||
![]() |
e3ee0df7ba | ||
![]() |
8f7ab280e2 | ||
![]() |
dbedc99421 | ||
![]() |
6cb359cb80 | ||
![]() |
ae2ad824a9 | ||
![]() |
02e3d7852b | ||
![]() |
3893a035be | ||
![]() |
658bdd9faa | ||
![]() |
e1eebcd4e0 | ||
![]() |
062b831e88 | ||
![]() |
b275efaeff | ||
![]() |
80d3033456 | ||
![]() |
bd0516f09a | ||
![]() |
df4d76e466 | ||
![]() |
dcbd7f8cad | ||
![]() |
73ec02ab9d | ||
![]() |
d1f8347071 | ||
![]() |
8601eedada | ||
![]() |
9afd33cdfc | ||
![]() |
5e1be8e558 | ||
![]() |
835dd2635a | ||
![]() |
f65b18c2f6 | ||
![]() |
b0e7b84f40 | ||
![]() |
1635db93c7 | ||
![]() |
c4fe462d11 | ||
![]() |
b1f403165d | ||
![]() |
46e4317b77 | ||
![]() |
e3ffbcadd8 | ||
![]() |
b7d73077e5 | ||
![]() |
77f61ee20a | ||
![]() |
8967f02fc9 | ||
![]() |
831ff6d0a9 | ||
![]() |
2199174def | ||
![]() |
55f41ddaab | ||
![]() |
21305d93bf | ||
![]() |
4478d5d904 | ||
![]() |
cc6253a6b8 | ||
![]() |
85f66853bc | ||
![]() |
7464fd149c | ||
![]() |
86f1a8019c | ||
![]() |
b98d56dcf6 | ||
![]() |
a3a8a5769d | ||
![]() |
4dd8bae5c9 | ||
![]() |
7ae45c42e7 | ||
![]() |
7551b4e7a3 | ||
![]() |
61bab22dde | ||
![]() |
6dcc23ebb6 | ||
![]() |
b06a574cc5 | ||
![]() |
b56f80b1b8 | ||
![]() |
20f6c05ec5 | ||
![]() |
57fce93af7 | ||
![]() |
110b6a1431 | ||
![]() |
53cafa9f3d | ||
![]() |
d4195deb3a | ||
![]() |
400ecfb79c | ||
![]() |
86151da271 | ||
![]() |
44f3e2557d | ||
![]() |
1f365c716e | ||
![]() |
9efcc41ab2 | ||
![]() |
13bbeeaceb | ||
![]() |
da4dcec14d | ||
![]() |
761c0b79c5 | ||
![]() |
d93ab0496f | ||
![]() |
66b6f9749d | ||
![]() |
17c2d1f26a | ||
![]() |
a79e632cdc | ||
![]() |
f36498421b | ||
![]() |
e45bbe4571 | ||
![]() |
fb5a84212c | ||
![]() |
dedc1c45a1 | ||
![]() |
6a12f9ff84 | ||
![]() |
641ed1873b | ||
![]() |
1d50da4b1c | ||
![]() |
c7897cca5d | ||
![]() |
4091b6f6b4 | ||
![]() |
766f35554e | ||
![]() |
c86137449e | ||
![]() |
efec01a92f | ||
![]() |
4fcad8e04b | ||
![]() |
4b4b2d790e | ||
![]() |
ec6ae343dd | ||
![]() |
b9fb361959 | ||
![]() |
a189298ab0 | ||
![]() |
714a2ad730 | ||
![]() |
fa8b96dfa1 | ||
![]() |
01955a0df8 | ||
![]() |
ac4aee39e3 | ||
![]() |
a0bca42a7a | ||
![]() |
af543dcd05 | ||
![]() |
af77109a47 | ||
![]() |
b979aff270 | ||
![]() |
b293e3fa75 | ||
![]() |
21eb37e471 | ||
![]() |
d3b60cc445 | ||
![]() |
7466e1d014 | ||
![]() |
2a7f28606c | ||
![]() |
0450482e6e | ||
![]() |
ee5d9b43b5 | ||
![]() |
3a8857c8c9 | ||
![]() |
be3dfb251d | ||
![]() |
4e11e29f70 | ||
![]() |
763bb95cea | ||
![]() |
668e7407e6 | ||
![]() |
c147ba1da1 | ||
![]() |
10f044c3dd | ||
![]() |
ce5140febd | ||
![]() |
858cdc86f5 | ||
![]() |
9a25297d51 | ||
![]() |
e24f7f5151 | ||
![]() |
eff5f1e119 | ||
![]() |
afcd362cd1 | ||
![]() |
0452be0cb3 | ||
![]() |
1624f10773 | ||
![]() |
8764be7461 | ||
![]() |
5dd15ef8e7 | ||
![]() |
4ac6366706 | ||
![]() |
adc0912efa | ||
![]() |
536823ce55 | ||
![]() |
207cd24edb | ||
![]() |
b039da1eba | ||
![]() |
8fcd0f3b6f | ||
![]() |
16fde6935c | ||
![]() |
9592cff9fa | ||
![]() |
109148988c | ||
![]() |
cf13fff7d2 | ||
![]() |
a9d8ac8bc0 | ||
![]() |
1a4717b366 | ||
![]() |
6cadf12260 | ||
![]() |
19d47784bd | ||
![]() |
b89102c5fc | ||
![]() |
4f20ebead3 | ||
![]() |
a9f89dbc64 | ||
![]() |
58ea1e07d2 | ||
![]() |
6de4c7e971 | ||
![]() |
03dc51ffa2 | ||
![]() |
aef2dcdfdd | ||
![]() |
0494119bf4 | ||
![]() |
0a17e21119 | ||
![]() |
52e2f926f4 | ||
![]() |
611fb279bc | ||
![]() |
41b4e64be9 | ||
![]() |
0d7315249d | ||
![]() |
4913766d58 | ||
![]() |
92da8c7044 | ||
![]() |
9dba3d5385 | ||
![]() |
2d3c26a4b2 | ||
![]() |
8eba2d3e50 | ||
![]() |
a8d4a27de1 | ||
![]() |
c42167c6f4 | ||
![]() |
44d182e2f9 | ||
![]() |
ad95e35687 | ||
![]() |
640a9995f4 | ||
![]() |
95625f6871 | ||
![]() |
2c20f72a9c | ||
![]() |
5ad788e768 | ||
![]() |
ed98c586c6 | ||
![]() |
3e865708d6 | ||
![]() |
c3bcbd63c0 | ||
![]() |
29e29439ee | ||
![]() |
0c19716f44 |
33
.github/workflows/development.yml
vendored
@ -5,27 +5,28 @@ jobs:
|
|||||||
name: golangci-lint
|
name: golangci-lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 20
|
fetch-depth: 20
|
||||||
- name: Run golangci-lint
|
- name: Run golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v3
|
||||||
with:
|
with:
|
||||||
version: v1.29
|
version: latest
|
||||||
args: "-v --new-from-rev HEAD~5"
|
args: "-v --new-from-rev HEAD~5 --timeout=5m"
|
||||||
test-build-upload:
|
test-build-upload:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.14.x, 1.15.x]
|
go-version: [1.22.x]
|
||||||
platform: [ubuntu-latest]
|
platform: [ubuntu-latest]
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
stable: false
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Test
|
- name: Test
|
||||||
@ -34,24 +35,24 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p output/{win,lin,arm,mac}
|
mkdir -p output/{win,lin,arm,mac}
|
||||||
VERSION=$(git describe --tags)
|
VERSION=$(git describe --tags)
|
||||||
GOOS=linux GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64
|
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
|
||||||
GOOS=windows GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe
|
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
|
||||||
GOOS=darwin GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64
|
CGO_ENABLED=0 GOOS=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
|
- name: Upload linux 64-bit
|
||||||
if: startsWith(matrix.go-version,'1.15')
|
if: startsWith(matrix.go-version,'1.22')
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: matterbridge-linux-64bit
|
name: matterbridge-linux-64bit
|
||||||
path: output/lin
|
path: output/lin
|
||||||
- name: Upload windows 64-bit
|
- name: Upload windows 64-bit
|
||||||
if: startsWith(matrix.go-version,'1.15')
|
if: startsWith(matrix.go-version,'1.22')
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: matterbridge-windows-64bit
|
name: matterbridge-windows-64bit
|
||||||
path: output/win
|
path: output/win
|
||||||
- name: Upload darwin 64-bit
|
- name: Upload darwin 64-bit
|
||||||
if: startsWith(matrix.go-version,'1.15')
|
if: startsWith(matrix.go-version,'1.22')
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: matterbridge-darwin-64bit
|
name: matterbridge-darwin-64bit
|
||||||
path: output/mac
|
path: output/mac
|
||||||
|
68
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
name: docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
with:
|
||||||
|
platforms: amd64,arm64
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
-
|
||||||
|
name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v3
|
||||||
|
with:
|
||||||
|
images: 42wim/matterbridge,ghcr.io/42wim/matterbridge
|
||||||
|
flavor: |
|
||||||
|
latest=true
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern=stable
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
-
|
||||||
|
name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Log into registry ghcr.io
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
3
.gitignore
vendored
@ -4,3 +4,6 @@
|
|||||||
|
|
||||||
# Exclude configuration file
|
# Exclude configuration file
|
||||||
matterbridge.toml
|
matterbridge.toml
|
||||||
|
|
||||||
|
# Exclude IDE Files
|
||||||
|
.vscode
|
||||||
|
@ -7,7 +7,7 @@ run:
|
|||||||
# concurrency: 4
|
# concurrency: 4
|
||||||
|
|
||||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
deadline: 2m
|
deadline: 5m
|
||||||
|
|
||||||
# exit code when at least one issue was found, default is 1
|
# exit code when at least one issue was found, default is 1
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
@ -91,7 +91,6 @@ linters-settings:
|
|||||||
# Correct spellings using locale preferences for US or UK.
|
# Correct spellings using locale preferences for US or UK.
|
||||||
# Default is to use a neutral variety of English.
|
# Default is to use a neutral variety of English.
|
||||||
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
||||||
locale: US
|
|
||||||
lll:
|
lll:
|
||||||
# max line length, lines longer will be reported. Default is 120.
|
# max line length, lines longer will be reported. Default is 120.
|
||||||
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
|
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
|
||||||
@ -183,7 +182,39 @@ linters:
|
|||||||
- interfacer
|
- interfacer
|
||||||
- goheader
|
- goheader
|
||||||
- noctx
|
- noctx
|
||||||
|
- gci
|
||||||
|
- errorlint
|
||||||
|
- nlreturn
|
||||||
|
- exhaustivestruct
|
||||||
|
- forbidigo
|
||||||
|
- wrapcheck
|
||||||
|
- varnamelen
|
||||||
|
- ireturn
|
||||||
|
- errorlint
|
||||||
|
- tparallel
|
||||||
|
- wrapcheck
|
||||||
|
- paralleltest
|
||||||
|
- makezero
|
||||||
|
- thelper
|
||||||
|
- cyclop
|
||||||
|
- revive
|
||||||
|
- importas
|
||||||
|
- gomoddirectives
|
||||||
|
- promlinter
|
||||||
|
- tagliatelle
|
||||||
|
- errname
|
||||||
|
- typecheck
|
||||||
|
- grouper
|
||||||
|
- decorder
|
||||||
|
- maintidx
|
||||||
|
- exhaustruct
|
||||||
|
- asasalint
|
||||||
|
- execinquery
|
||||||
|
- nosnakecase
|
||||||
|
- exhaustive
|
||||||
|
- testifylint
|
||||||
|
- mnd
|
||||||
|
- depguard
|
||||||
# rules to deal with reported isues
|
# rules to deal with reported isues
|
||||||
issues:
|
issues:
|
||||||
# List of regexps of issue texts to exclude, empty list by default.
|
# List of regexps of issue texts to exclude, empty list by default.
|
||||||
|
@ -18,8 +18,11 @@ builds:
|
|||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
- 386
|
- 386
|
||||||
|
goarm:
|
||||||
|
- 6
|
||||||
|
- 7
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X main.githash={{.ShortCommit}}
|
- -s -w -X github.com/42wim/matterbridge/version.GitHash={{.ShortCommit}}
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
-
|
-
|
||||||
|
10
Dockerfile
@ -1,11 +1,9 @@
|
|||||||
FROM alpine AS builder
|
FROM alpine AS builder
|
||||||
|
|
||||||
COPY . /go/src/github.com/42wim/matterbridge
|
COPY . /go/src/matterbridge
|
||||||
RUN apk update && apk add go git gcc musl-dev \
|
RUN apk --no-cache add go git \
|
||||||
&& cd /go/src/github.com/42wim/matterbridge \
|
&& cd /go/src/matterbridge \
|
||||||
&& export GOPATH=/go \
|
&& CGO_ENABLED=0 go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
|
||||||
&& go get \
|
|
||||||
&& go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
|
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
RUN apk --no-cache add ca-certificates mailcap
|
RUN apk --no-cache add ca-certificates mailcap
|
||||||
|
14
Dockerfile_whatsappmulti
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM alpine AS builder
|
||||||
|
|
||||||
|
COPY . /go/src/matterbridge
|
||||||
|
RUN apk --no-cache add go git \
|
||||||
|
&& cd /go/src/matterbridge \
|
||||||
|
&& CGO_ENABLED=0 go build -tags whatsappmulti -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
|
||||||
|
|
||||||
|
FROM alpine
|
||||||
|
RUN apk --no-cache add ca-certificates mailcap
|
||||||
|
COPY --from=builder /bin/matterbridge /bin/matterbridge
|
||||||
|
RUN mkdir /etc/matterbridge \
|
||||||
|
&& touch /etc/matterbridge/matterbridge.toml \
|
||||||
|
&& ln -sf /matterbridge.toml /etc/matterbridge/matterbridge.toml
|
||||||
|
ENTRYPOINT ["/bin/matterbridge", "-conf", "/etc/matterbridge/matterbridge.toml"]
|
134
README.md
@ -58,20 +58,22 @@ And more...
|
|||||||
- [Binaries](#binaries)
|
- [Binaries](#binaries)
|
||||||
- [Packages](#packages)
|
- [Packages](#packages)
|
||||||
- [Building](#building)
|
- [Building](#building)
|
||||||
|
- [Building with whatsapp (beta) multidevice support](#building-with-whatsapp-beta-multidevice-support)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Basic configuration](#basic-configuration)
|
- [Basic configuration](#basic-configuration)
|
||||||
- [Settings](#settings)
|
- [Settings](#settings)
|
||||||
- [Advanced configuration](#advanced-configuration)
|
- [Advanced configuration](#advanced-configuration)
|
||||||
- [Examples](#examples)
|
- [Examples](#examples)
|
||||||
- [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing)
|
- [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing)
|
||||||
- [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general)
|
- [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general)
|
||||||
- [Running](#running)
|
- [Running](#running)
|
||||||
- [Docker](#docker)
|
- [Docker](#docker)
|
||||||
- [Changelog](#changelog)
|
- [Systemd](#systemd)
|
||||||
- [FAQ](#faq)
|
- [Changelog](#changelog)
|
||||||
- [Related projects](#related-projects)
|
- [FAQ](#faq)
|
||||||
- [Articles](#articles)
|
- [Related projects](#related-projects)
|
||||||
- [Thanks](#thanks)
|
- [Articles / Tutorials](#articles--tutorials)
|
||||||
|
- [Thanks](#thanks)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -88,31 +90,45 @@ And more...
|
|||||||
|
|
||||||
- [Discord](https://discordapp.com)
|
- [Discord](https://discordapp.com)
|
||||||
- [Gitter](https://gitter.im)
|
- [Gitter](https://gitter.im)
|
||||||
|
- [Harmony](https://harmonyapp.io)
|
||||||
- [IRC](http://www.mirc.com/servers.html)
|
- [IRC](http://www.mirc.com/servers.html)
|
||||||
- [Keybase](https://keybase.io)
|
- [Keybase](https://keybase.io)
|
||||||
- [Matrix](https://matrix.org)
|
- [Matrix](https://matrix.org)
|
||||||
- [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x
|
- [Mattermost](https://github.com/mattermost/mattermost-server/)
|
||||||
- [Microsoft Teams](https://teams.microsoft.com)
|
- [Microsoft Teams](https://teams.microsoft.com)
|
||||||
- [Mumble](https://www.mumble.info/)
|
- [Mumble](https://www.mumble.info/)
|
||||||
- [Nextcloud Talk](https://nextcloud.com/talk/)
|
- [Nextcloud Talk](https://nextcloud.com/talk/)
|
||||||
- [Rocket.chat](https://rocket.chat)
|
- [Rocket.chat](https://rocket.chat)
|
||||||
- [Slack](https://slack.com)
|
- [Slack](https://slack.com)
|
||||||
- [Ssh-chat](https://github.com/shazow/ssh-chat)
|
- [Ssh-chat](https://github.com/shazow/ssh-chat)
|
||||||
- [Steam](https://store.steampowered.com/)
|
- ~~[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)
|
- [Telegram](https://telegram.org)
|
||||||
- [Twitch](https://twitch.tv)
|
- [Twitch](https://twitch.tv)
|
||||||
|
- [VK](https://vk.com/)
|
||||||
- [WhatsApp](https://www.whatsapp.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)
|
- [XMPP](https://xmpp.org)
|
||||||
- [Zulip](https://zulipchat.com)
|
- [Zulip](https://zulipchat.com)
|
||||||
|
|
||||||
### 3rd party via matterbridge api
|
### 3rd party via matterbridge api
|
||||||
|
|
||||||
|
- [Delta Chat](https://github.com/deltachat-bot/matterdelta)
|
||||||
|
- [Minecraft](https://github.com/raws/mattercraft)
|
||||||
|
- [Minecraft](https://gitlab.com/Programie/MatterBukkit)
|
||||||
|
|
||||||
|
#### Past 3rd party projects
|
||||||
- [Discourse](https://github.com/DeclanHoare/matterbabble)
|
- [Discourse](https://github.com/DeclanHoare/matterbabble)
|
||||||
|
- [Facebook messenger](https://github.com/powerjungle/fbridge-asyncio)
|
||||||
- [Facebook messenger](https://github.com/VictorNine/fbridge)
|
- [Facebook messenger](https://github.com/VictorNine/fbridge)
|
||||||
- [Minecraft](https://github.com/elytra/MatterLink)
|
- [Minecraft](https://github.com/elytra/MatterLink)
|
||||||
- [Reddit](https://github.com/bonehurtingjuice/mattereddit)
|
- [Reddit](https://github.com/bonehurtingjuice/mattereddit)
|
||||||
- [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
|
- [MatterAMXX](https://github.com/andrewlindberg/MatterAMXX): [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
|
||||||
- [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)
|
||||||
|
|
||||||
|
|
||||||
### API
|
### API
|
||||||
|
|
||||||
@ -121,12 +137,19 @@ More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/
|
|||||||
|
|
||||||
Used by the projects below. Feel free to make a PR to add your project to this list.
|
Used by the projects below. Feel free to make a PR to add your project to this list.
|
||||||
|
|
||||||
- [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Server chat)
|
- [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Forge server chat, archived)
|
||||||
|
- [MatterCraft](https://github.com/raws/mattercraft) (Matterbridge link for Minecraft Forge server chat)
|
||||||
|
- [MatterBukkit](https://gitlab.com/Programie/MatterBukkit) (Matterbridge link for Minecraft Bukkit/Spigot server chat)
|
||||||
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
|
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
|
||||||
- [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
|
- [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
|
||||||
|
- [fbridge-asyncio](https://github.com/powerjungle/fbridge-asyncio) (Facebook messenger support)
|
||||||
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
|
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
|
||||||
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
|
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
|
||||||
- [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod)
|
- [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod)
|
||||||
|
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
|
||||||
|
- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
|
||||||
|
- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
|
||||||
|
- [beerchat](https://github.com/mt-mods/beerchat) (Matterbridge link for minetest)
|
||||||
|
|
||||||
## Chat with us
|
## Chat with us
|
||||||
|
|
||||||
@ -153,25 +176,71 @@ See <https://github.com/42wim/matterbridge/wiki>
|
|||||||
|
|
||||||
### Binaries
|
### Binaries
|
||||||
|
|
||||||
- Latest stable release [v1.20.0](https://github.com/42wim/matterbridge/releases/latest)
|
- Latest stable release [v1.26.0](https://github.com/42wim/matterbridge/releases/latest)
|
||||||
- Development releases (follows master) can be downloaded [here](https://github.com/42wim/matterbridge/actions) selecting the latest green build and then artifacts.
|
- Development releases (follows master) can be downloaded [here](https://github.com/42wim/matterbridge/actions) selecting the latest green build and then artifacts.
|
||||||
|
|
||||||
To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest) and follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
|
To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest). On \*nix platforms you may need to make the binary executable - you can do this by running `chmod a+x` on the binary (example: `chmod a+x matterbridge-1.24.1-linux-64bit`). After downloading (and making the binary executable, if necessary), follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
|
||||||
|
|
||||||
### Packages
|
### Packages
|
||||||
|
|
||||||
- [Overview](https://repology.org/metapackage/matterbridge/versions)
|
- [Overview](https://repology.org/metapackage/matterbridge/versions)
|
||||||
- [snap](https://snapcraft.io/matterbridge)
|
- [snap](https://snapcraft.io/matterbridge)
|
||||||
|
- [scoop](https://github.com/42wim/scoop-bucket)
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest)
|
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest)
|
||||||
|
|
||||||
If you really want to build from source, follow these instructions:
|
If you really want to build from source, follow these instructions:
|
||||||
Go 1.12+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed.
|
Go 1.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
|
```bash
|
||||||
go get github.com/42wim/matterbridge
|
go install github.com/42wim/matterbridge
|
||||||
|
```
|
||||||
|
|
||||||
|
To install the latest dev run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/42wim/matterbridge@master
|
||||||
|
```
|
||||||
|
|
||||||
|
To install the latest stable run without msteams or zulip bridge:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install -tags nomsteams,nozulip github.com/42wim/matterbridge
|
||||||
|
```
|
||||||
|
|
||||||
|
You should now have matterbridge binary in the ~/go/bin directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ls ~/go/bin/
|
||||||
|
matterbridge
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building with whatsapp (beta) multidevice support
|
||||||
|
|
||||||
|
Because the library we use for Whatsapp multidevice support includes a GPL3 library we can not provide you binaries.
|
||||||
|
(as this would require the Matterbridge to change it license to GPL)
|
||||||
|
|
||||||
|
Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`)
|
||||||
|
|
||||||
|
So this means you have to build it yourself using the instructions below:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install -tags whatsappmulti github.com/42wim/matterbridge@master
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're low on memory and don't need msteams:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install -tags nomsteams,whatsappmulti github.com/42wim/matterbridge@master
|
||||||
```
|
```
|
||||||
|
|
||||||
You should now have matterbridge binary in the ~/go/bin directory:
|
You should now have matterbridge binary in the ~/go/bin directory:
|
||||||
@ -201,8 +270,8 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[irc]
|
[irc]
|
||||||
[irc.freenode]
|
[irc.libera]
|
||||||
Server="irc.freenode.net:6667"
|
Server="irc.libera.chat:6667"
|
||||||
Nick="yourbotname"
|
Nick="yourbotname"
|
||||||
|
|
||||||
[mattermost]
|
[mattermost]
|
||||||
@ -218,7 +287,7 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for
|
|||||||
name="mygateway"
|
name="mygateway"
|
||||||
enable=true
|
enable=true
|
||||||
[[gateway.inout]]
|
[[gateway.inout]]
|
||||||
account="irc.freenode"
|
account="irc.libera"
|
||||||
channel="#testing"
|
channel="#testing"
|
||||||
|
|
||||||
[[gateway.inout]]
|
[[gateway.inout]]
|
||||||
@ -275,6 +344,10 @@ Usage of ./matterbridge:
|
|||||||
|
|
||||||
Please take a look at the [Docker Wiki page](https://github.com/42wim/matterbridge/wiki/Deploy:-Docker) for more information.
|
Please take a look at the [Docker Wiki page](https://github.com/42wim/matterbridge/wiki/Deploy:-Docker) for more information.
|
||||||
|
|
||||||
|
### Systemd
|
||||||
|
|
||||||
|
Please take a look at the [Service Files page](https://github.com/42wim/matterbridge/wiki/Service-files) for more information.
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md)
|
See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md)
|
||||||
@ -296,8 +369,13 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
|
|||||||
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
|
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
|
||||||
- [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
|
- [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
|
||||||
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
|
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
|
||||||
|
- [nextcloud talk](https://github.com/nextcloud/talk_matterbridge) (Integrates matterbridge in Nextcloud Talk)
|
||||||
|
- [mattercraft](https://github.com/raws/mattercraft) (Minecraft bridge)
|
||||||
|
- [vs-matterbridge](https://github.com/NikkyAI/vs-matterbridge) (Vintage Story bridge)
|
||||||
|
- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
|
||||||
|
- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
|
||||||
|
|
||||||
## Articles
|
## Articles / Tutorials
|
||||||
|
|
||||||
- [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
|
- [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
|
||||||
- <https://mattermost.com/blog/connect-irc-to-mattermost/>
|
- <https://mattermost.com/blog/connect-irc-to-mattermost/>
|
||||||
@ -308,6 +386,9 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
|
|||||||
- <https://www.stitcher.com/s/?eid=52382713>
|
- <https://www.stitcher.com/s/?eid=52382713>
|
||||||
- <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/>
|
- <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/>
|
||||||
- <https://userlinux.net/mattermost-and-matterbridge.html>
|
- <https://userlinux.net/mattermost-and-matterbridge.html>
|
||||||
|
- <https://nextcloud.com/blog/bridging-chat-services-in-talk/>
|
||||||
|
- <https://minecraftchest1.wordpress.com/2021/06/05/how-to-install-and-setup-matterbridge/>
|
||||||
|
- Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc)
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
||||||
@ -322,10 +403,10 @@ Matterbridge wouldn't exist without these libraries:
|
|||||||
|
|
||||||
- discord - <https://github.com/bwmarrin/discordgo>
|
- discord - <https://github.com/bwmarrin/discordgo>
|
||||||
- echo - <https://github.com/labstack/echo>
|
- echo - <https://github.com/labstack/echo>
|
||||||
- gitter - <https://github.com/sromku/go-gitter>
|
|
||||||
- gops - <https://github.com/google/gops>
|
- gops - <https://github.com/google/gops>
|
||||||
- gozulipbot - <https://github.com/ifo/gozulipbot>
|
- gozulipbot - <https://github.com/ifo/gozulipbot>
|
||||||
- gumble - <https://github.com/layeh/gumble>
|
- gumble - <https://github.com/layeh/gumble>
|
||||||
|
- harmony - <https://github.com/harmony-development/shibshib>
|
||||||
- irc - <https://github.com/lrstanley/girc>
|
- irc - <https://github.com/lrstanley/girc>
|
||||||
- keybase - <https://github.com/keybase/go-keybase-chat-bot>
|
- keybase - <https://github.com/keybase/go-keybase-chat-bot>
|
||||||
- matrix - <https://github.com/matrix-org/gomatrix>
|
- matrix - <https://github.com/matrix-org/gomatrix>
|
||||||
@ -333,12 +414,15 @@ Matterbridge wouldn't exist without these libraries:
|
|||||||
- msgraph.go - <https://github.com/yaegashi/msgraph.go>
|
- msgraph.go - <https://github.com/yaegashi/msgraph.go>
|
||||||
- mumble - <https://github.com/layeh/gumble>
|
- mumble - <https://github.com/layeh/gumble>
|
||||||
- nctalk - <https://github.com/gary-kim/go-nc-talk>
|
- nctalk - <https://github.com/gary-kim/go-nc-talk>
|
||||||
|
- rocketchat - <https://github.com/RocketChat/Rocket.Chat.Go.SDK>
|
||||||
- slack - <https://github.com/nlopes/slack>
|
- slack - <https://github.com/nlopes/slack>
|
||||||
- sshchat - <https://github.com/shazow/ssh-chat>
|
- sshchat - <https://github.com/shazow/ssh-chat>
|
||||||
- steam - <https://github.com/Philipp15b/go-steam>
|
- steam - <https://github.com/Philipp15b/go-steam>
|
||||||
- telegram - <https://github.com/go-telegram-bot-api/telegram-bot-api>
|
- telegram - <https://github.com/go-telegram-bot-api/telegram-bot-api>
|
||||||
- tengo - <https://github.com/d5/tengo>
|
- tengo - <https://github.com/d5/tengo>
|
||||||
|
- vk - <https://github.com/SevereCloud/vksdk>
|
||||||
- whatsapp - <https://github.com/Rhymen/go-whatsapp>
|
- whatsapp - <https://github.com/Rhymen/go-whatsapp>
|
||||||
|
- whatsapp - <https://github.com/tulir/whatsmeow>
|
||||||
- xmpp - <https://github.com/mattn/go-xmpp>
|
- xmpp - <https://github.com/mattn/go-xmpp>
|
||||||
- zulip - <https://github.com/ifo/gozulipbot>
|
- zulip - <https://github.com/ifo/gozulipbot>
|
||||||
|
|
||||||
@ -346,7 +430,7 @@ Matterbridge wouldn't exist without these libraries:
|
|||||||
|
|
||||||
[mb-discord]: https://discord.gg/AkKPtrQ
|
[mb-discord]: https://discord.gg/AkKPtrQ
|
||||||
[mb-gitter]: https://gitter.im/42wim/matterbridge
|
[mb-gitter]: https://gitter.im/42wim/matterbridge
|
||||||
[mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat
|
[mb-irc]: https://web.libera.chat/#matterbridge
|
||||||
[mb-keybase]: https://keybase.io/team/matterbridge
|
[mb-keybase]: https://keybase.io/team/matterbridge
|
||||||
[mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org
|
[mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org
|
||||||
[mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e
|
[mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/olahol/melody.v1"
|
"github.com/olahol/melody"
|
||||||
|
|
||||||
"github.com/42wim/matterbridge/bridge"
|
"github.com/42wim/matterbridge/bridge"
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
ring "github.com/zfjagann/golang-ring"
|
ring "github.com/zfjagann/golang-ring"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -137,6 +140,36 @@ func (b *API) handlePostMessage(c echo.Context) error {
|
|||||||
message.Account = b.Account
|
message.Account = b.Account
|
||||||
message.ID = ""
|
message.ID = ""
|
||||||
message.Timestamp = time.Now()
|
message.Timestamp = time.Now()
|
||||||
|
|
||||||
|
var (
|
||||||
|
fm map[string]interface{}
|
||||||
|
ds string
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, f := range message.Extra["file"] {
|
||||||
|
fi := config.FileInfo{}
|
||||||
|
if fm, ok = f.(map[string]interface{}); !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "invalid format for extra")
|
||||||
|
}
|
||||||
|
err := mapstructure.Decode(fm, &fi)
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "got string") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// mapstructure doesn't decode base64 into []byte, so it must be done manually for fi.Data
|
||||||
|
if ds, ok = fm["Data"].(string); !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "invalid format for data")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := base64.StdEncoding.DecodeString(ds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fi.Data = &data
|
||||||
|
message.Extra["file"][i] = fi
|
||||||
|
}
|
||||||
b.Log.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
|
b.Log.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
|
||||||
b.Remote <- message
|
b.Remote <- message
|
||||||
return c.JSON(http.StatusOK, message)
|
return c.JSON(http.StatusOK, message)
|
||||||
@ -166,15 +199,20 @@ func (b *API) handleStream(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
c.Response().Flush()
|
c.Response().Flush()
|
||||||
for {
|
for {
|
||||||
|
select {
|
||||||
// TODO: this causes issues, messages should be broadcasted to all connected clients
|
// TODO: this causes issues, messages should be broadcasted to all connected clients
|
||||||
msg := b.Messages.Dequeue()
|
default:
|
||||||
if msg != nil {
|
msg := b.Messages.Dequeue()
|
||||||
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
|
if msg != nil {
|
||||||
return err
|
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Response().Flush()
|
||||||
}
|
}
|
||||||
c.Response().Flush()
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
case <-c.Request().Context().Done():
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,12 +23,15 @@ const (
|
|||||||
EventRejoinChannels = "rejoin_channels"
|
EventRejoinChannels = "rejoin_channels"
|
||||||
EventUserAction = "user_action"
|
EventUserAction = "user_action"
|
||||||
EventMsgDelete = "msg_delete"
|
EventMsgDelete = "msg_delete"
|
||||||
|
EventFileDelete = "file_delete"
|
||||||
EventAPIConnected = "api_connected"
|
EventAPIConnected = "api_connected"
|
||||||
EventUserTyping = "user_typing"
|
EventUserTyping = "user_typing"
|
||||||
EventGetChannelMembers = "get_channel_members"
|
EventGetChannelMembers = "get_channel_members"
|
||||||
EventNoticeIRC = "notice_irc"
|
EventNoticeIRC = "notice_irc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ParentIDNotFound = "msg-parent-not-found"
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
@ -45,14 +48,23 @@ type Message struct {
|
|||||||
Extra map[string][]interface{}
|
Extra map[string][]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Message) ParentNotFound() bool {
|
||||||
|
return m.ParentID == ParentIDNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Message) ParentValid() bool {
|
||||||
|
return m.ParentID != "" && !m.ParentNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Data *[]byte
|
Data *[]byte
|
||||||
Comment string
|
Comment string
|
||||||
URL string
|
URL string
|
||||||
Size int64
|
Size int64
|
||||||
Avatar bool
|
Avatar bool
|
||||||
SHA string
|
SHA string
|
||||||
|
NativeID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChannelInfo struct {
|
type ChannelInfo struct {
|
||||||
@ -75,27 +87,28 @@ type ChannelMember struct {
|
|||||||
type ChannelMembers []ChannelMember
|
type ChannelMembers []ChannelMember
|
||||||
|
|
||||||
type Protocol struct {
|
type Protocol struct {
|
||||||
AuthCode string // steam
|
AllowMention []string // discord
|
||||||
BindAddress string // mattermost, slack // DEPRECATED
|
AuthCode string // steam
|
||||||
Buffer int // api
|
BindAddress string // mattermost, slack // DEPRECATED
|
||||||
Charset string // irc
|
Buffer int // api
|
||||||
ClientID string // msteams
|
Charset string // irc
|
||||||
ColorNicks bool // only irc for now
|
ClientID string // msteams
|
||||||
Debug bool // general
|
ColorNicks bool // only irc for now
|
||||||
DebugLevel int // only for irc now
|
Debug bool // general
|
||||||
DisableWebPagePreview bool // telegram
|
DebugLevel int // only for irc now
|
||||||
EditSuffix string // mattermost, slack, discord, telegram, gitter
|
DisableWebPagePreview bool // telegram
|
||||||
EditDisable bool // mattermost, slack, discord, telegram, gitter
|
EditSuffix string // mattermost, slack, discord, telegram, gitter
|
||||||
HTMLDisable bool // matrix
|
EditDisable bool // mattermost, slack, discord, telegram, gitter
|
||||||
IconURL string // mattermost, slack
|
HTMLDisable bool // matrix
|
||||||
IgnoreFailureOnStart bool // general
|
IconURL string // mattermost, slack
|
||||||
IgnoreNicks string // all protocols
|
IgnoreFailureOnStart bool // general
|
||||||
IgnoreMessages string // all protocols
|
IgnoreNicks string // all protocols
|
||||||
Jid string // xmpp
|
IgnoreMessages string // all protocols
|
||||||
JoinDelay string // all protocols
|
Jid string // xmpp
|
||||||
Label string // all protocols
|
JoinDelay string // all protocols
|
||||||
Login string // mattermost, matrix
|
Label string // all protocols
|
||||||
LogFile string // general
|
Login string // mattermost, matrix
|
||||||
|
LogFile string // general
|
||||||
MediaDownloadBlackList []string
|
MediaDownloadBlackList []string
|
||||||
MediaDownloadPath string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server.
|
MediaDownloadPath string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server.
|
||||||
MediaDownloadSize int // all protocols
|
MediaDownloadSize int // all protocols
|
||||||
@ -108,7 +121,9 @@ type Protocol struct {
|
|||||||
MessageLength int // IRC, max length of a message allowed
|
MessageLength int // IRC, max length of a message allowed
|
||||||
MessageQueue int // IRC, size of message queue for flood control
|
MessageQueue int // IRC, size of message queue for flood control
|
||||||
MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping
|
MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping
|
||||||
|
MessageSplitMaxCount int // discord, split long messages into at most this many messages instead of clipping (MessageLength=1950 cannot be configured)
|
||||||
Muc string // xmpp
|
Muc string // xmpp
|
||||||
|
MxID string // matrix
|
||||||
Name string // all protocols
|
Name string // all protocols
|
||||||
Nick string // all protocols
|
Nick string // all protocols
|
||||||
NickFormatter string // mattermost, slack
|
NickFormatter string // mattermost, slack
|
||||||
@ -126,12 +141,13 @@ type Protocol struct {
|
|||||||
QuoteDisable bool // telegram
|
QuoteDisable bool // telegram
|
||||||
QuoteFormat string // telegram
|
QuoteFormat string // telegram
|
||||||
QuoteLengthLimit int // telegram
|
QuoteLengthLimit int // telegram
|
||||||
|
RealName string // IRC
|
||||||
RejoinDelay int // IRC
|
RejoinDelay int // IRC
|
||||||
ReplaceMessages [][]string // all protocols
|
ReplaceMessages [][]string // all protocols
|
||||||
ReplaceNicks [][]string // all protocols
|
ReplaceNicks [][]string // all protocols
|
||||||
RemoteNickFormat string // all protocols
|
RemoteNickFormat string // all protocols
|
||||||
RunCommands []string // IRC
|
RunCommands []string // IRC
|
||||||
Server string // IRC,mattermost,XMPP,discord
|
Server string // IRC,mattermost,XMPP,discord,matrix
|
||||||
SessionFile string // msteams,whatsapp
|
SessionFile string // msteams,whatsapp
|
||||||
ShowJoinPart bool // all protocols
|
ShowJoinPart bool // all protocols
|
||||||
ShowTopicChange bool // slack
|
ShowTopicChange bool // slack
|
||||||
@ -146,7 +162,7 @@ type Protocol struct {
|
|||||||
Team string // mattermost, keybase
|
Team string // mattermost, keybase
|
||||||
TeamID string // msteams
|
TeamID string // msteams
|
||||||
TenantID string // msteams
|
TenantID string // msteams
|
||||||
Token string // gitter, slack, discord, api
|
Token string // gitter, slack, discord, api, matrix
|
||||||
Topic string // zulip
|
Topic string // zulip
|
||||||
URL string // mattermost, slack // DEPRECATED
|
URL string // mattermost, slack // DEPRECATED
|
||||||
UseAPI bool // mattermost, slack
|
UseAPI bool // mattermost, slack
|
||||||
@ -155,8 +171,9 @@ type Protocol struct {
|
|||||||
UseTLS bool // IRC
|
UseTLS bool // IRC
|
||||||
UseDiscriminator bool // discord
|
UseDiscriminator bool // discord
|
||||||
UseFirstName bool // telegram
|
UseFirstName bool // telegram
|
||||||
UseUserName bool // discord, matrix
|
UseUserName bool // discord, matrix, mattermost
|
||||||
UseInsecureURL bool // telegram
|
UseInsecureURL bool // telegram
|
||||||
|
UserName string // IRC
|
||||||
VerboseJoinPart bool // IRC
|
VerboseJoinPart bool // IRC
|
||||||
WebhookBindAddress string // mattermost, slack
|
WebhookBindAddress string // mattermost, slack
|
||||||
WebhookURL string // mattermost, slack
|
WebhookURL string // mattermost, slack
|
||||||
|
@ -2,30 +2,31 @@ package bdiscord
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/42wim/matterbridge/bridge"
|
"github.com/42wim/matterbridge/bridge"
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/bridge/discord/transmitter"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
"github.com/matterbridge/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
lru "github.com/hashicorp/golang-lru"
|
||||||
)
|
)
|
||||||
|
|
||||||
const MessageLength = 1950
|
const (
|
||||||
|
MessageLength = 1950
|
||||||
|
cFileUpload = "file_upload"
|
||||||
|
)
|
||||||
|
|
||||||
type Bdiscord struct {
|
type Bdiscord struct {
|
||||||
*bridge.Config
|
*bridge.Config
|
||||||
|
|
||||||
c *discordgo.Session
|
c *discordgo.Session
|
||||||
|
|
||||||
nick string
|
nick string
|
||||||
userID string
|
userID string
|
||||||
guildID string
|
guildID string
|
||||||
webhookID string
|
|
||||||
webhookToken string
|
|
||||||
canEditWebhooks bool
|
|
||||||
|
|
||||||
channelsMutex sync.RWMutex
|
channelsMutex sync.RWMutex
|
||||||
channels []*discordgo.Channel
|
channels []*discordgo.Channel
|
||||||
@ -34,30 +35,39 @@ type Bdiscord struct {
|
|||||||
membersMutex sync.RWMutex
|
membersMutex sync.RWMutex
|
||||||
userMemberMap map[string]*discordgo.Member
|
userMemberMap map[string]*discordgo.Member
|
||||||
nickMemberMap map[string]*discordgo.Member
|
nickMemberMap map[string]*discordgo.Member
|
||||||
|
|
||||||
|
// Webhook specific logic
|
||||||
|
useAutoWebhooks bool
|
||||||
|
transmitter *transmitter.Transmitter
|
||||||
|
cache *lru.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *bridge.Config) bridge.Bridger {
|
func New(cfg *bridge.Config) bridge.Bridger {
|
||||||
b := &Bdiscord{Config: cfg}
|
newCache, err := lru.New(5000)
|
||||||
|
if err != nil {
|
||||||
|
cfg.Log.Fatalf("Could not create LRU cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &Bdiscord{
|
||||||
|
Config: cfg,
|
||||||
|
cache: newCache,
|
||||||
|
}
|
||||||
|
|
||||||
b.userMemberMap = make(map[string]*discordgo.Member)
|
b.userMemberMap = make(map[string]*discordgo.Member)
|
||||||
b.nickMemberMap = make(map[string]*discordgo.Member)
|
b.nickMemberMap = make(map[string]*discordgo.Member)
|
||||||
b.channelInfoMap = make(map[string]*config.ChannelInfo)
|
b.channelInfoMap = make(map[string]*config.ChannelInfo)
|
||||||
if b.GetString("WebhookURL") != "" {
|
|
||||||
b.Log.Debug("Configuring Discord Incoming Webhook")
|
b.useAutoWebhooks = b.GetBool("AutoWebhooks")
|
||||||
b.webhookID, b.webhookToken = b.splitURL(b.GetString("WebhookURL"))
|
if b.useAutoWebhooks {
|
||||||
|
b.Log.Debug("Using automatic webhooks")
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bdiscord) Connect() error {
|
func (b *Bdiscord) Connect() error {
|
||||||
var err error
|
var err error
|
||||||
var guildFound bool
|
|
||||||
token := b.GetString("Token")
|
token := b.GetString("Token")
|
||||||
b.Log.Info("Connecting")
|
b.Log.Info("Connecting")
|
||||||
if b.GetString("WebhookURL") == "" {
|
|
||||||
b.Log.Info("Connecting using token")
|
|
||||||
} else {
|
|
||||||
b.Log.Info("Connecting using webhookurl (for posting) and token")
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
|
if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
|
||||||
token = "Bot " + b.GetString("Token")
|
token = "Bot " + b.GetString("Token")
|
||||||
}
|
}
|
||||||
@ -71,19 +81,16 @@ func (b *Bdiscord) Connect() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b.Log.Info("Connection succeeded")
|
b.Log.Info("Connection succeeded")
|
||||||
b.c.AddHandler(b.messageCreate)
|
// Add privileged intent for guild member tracking. This is needed to track nicks
|
||||||
b.c.AddHandler(b.messageTyping)
|
// for display names and @mention translation
|
||||||
b.c.AddHandler(b.memberUpdate)
|
b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
|
||||||
b.c.AddHandler(b.messageUpdate)
|
discordgo.IntentsGuildMembers)
|
||||||
b.c.AddHandler(b.messageDelete)
|
|
||||||
b.c.AddHandler(b.messageDeleteBulk)
|
|
||||||
b.c.AddHandler(b.memberAdd)
|
|
||||||
b.c.AddHandler(b.memberRemove)
|
|
||||||
err = b.c.Open()
|
err = b.c.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
guilds, err := b.c.UserGuilds(100, "", "")
|
guilds, err := b.c.UserGuilds(100, "", "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -94,66 +101,107 @@ func (b *Bdiscord) Connect() error {
|
|||||||
serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
|
serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
|
||||||
b.nick = userinfo.Username
|
b.nick = userinfo.Username
|
||||||
b.userID = userinfo.ID
|
b.userID = userinfo.ID
|
||||||
|
|
||||||
|
// Try and find this account's guild, and populate channels
|
||||||
b.channelsMutex.Lock()
|
b.channelsMutex.Lock()
|
||||||
for _, guild := range guilds {
|
for _, guild := range guilds {
|
||||||
if guild.Name == serverName || guild.ID == serverName {
|
// Skip, if the server name does not match the visible name or the ID
|
||||||
b.channels, err = b.c.GuildChannels(guild.ID)
|
if guild.Name != serverName && guild.ID != serverName {
|
||||||
if err != nil {
|
continue
|
||||||
break
|
|
||||||
}
|
|
||||||
b.guildID = guild.ID
|
|
||||||
guildFound = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Complain about an ambiguous Server setting. Two Discord servers could have the same title!
|
||||||
|
// For IDs, practically this will never happen. It would only trigger if some server's name is also an ID.
|
||||||
|
if b.guildID != "" {
|
||||||
|
return fmt.Errorf("found multiple Discord servers with the same name %#v, expected to see only one", serverName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getting this guild's channel could result in a permission error
|
||||||
|
b.channels, err = b.c.GuildChannels(guild.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get %#v's channels: %w", b.GetString("Server"), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.guildID = guild.ID
|
||||||
}
|
}
|
||||||
b.channelsMutex.Unlock()
|
b.channelsMutex.Unlock()
|
||||||
if !guildFound {
|
|
||||||
msg := fmt.Sprintf("Server \"%s\" not found", b.GetString("Server"))
|
// If we couldn't find a guild, we print extra debug information and return a nice error
|
||||||
err = errors.New(msg)
|
if b.guildID == "" {
|
||||||
b.Log.Error(msg)
|
err = fmt.Errorf("could not find Discord server %#v", b.GetString("Server"))
|
||||||
b.Log.Info("Possible values:")
|
b.Log.Error(err.Error())
|
||||||
|
|
||||||
|
// Print all of the possible server values
|
||||||
|
b.Log.Info("Possible server values:")
|
||||||
for _, guild := range guilds {
|
for _, guild := range guilds {
|
||||||
b.Log.Infof("Server=\"%s\" # Server name", guild.Name)
|
b.Log.Infof("\t- Server=%#v # by name", guild.Name)
|
||||||
b.Log.Infof("Server=\"%s\" # Server ID", guild.ID)
|
b.Log.Infof("\t- Server=%#v # by ID", guild.ID)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if err != nil {
|
// If there are no results, we should say that
|
||||||
|
if len(guilds) == 0 {
|
||||||
|
b.Log.Info("\t- (none found)")
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.channelsMutex.RLock()
|
// Legacy note: WebhookURL used to have an actual webhook URL that we would edit,
|
||||||
if b.GetString("WebhookURL") == "" {
|
// but we stopped doing that due to Discord making rate limits more aggressive.
|
||||||
for _, channel := range b.channels {
|
//
|
||||||
b.Log.Debugf("found channel %#v", channel)
|
// Even older: the same WebhookURL used to be used by every channel, which is usually unexpected.
|
||||||
}
|
// This is no longer possible.
|
||||||
} else {
|
if b.GetString("WebhookURL") != "" {
|
||||||
manageWebhooks := discordgo.PermissionManageWebhooks
|
message := "The global WebhookURL setting has been removed. "
|
||||||
var channelsDenied []string
|
message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. "
|
||||||
for _, info := range b.Channels {
|
message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections."
|
||||||
id := b.getChannelID(info.Name) // note(qaisjp): this readlocks channelsMutex
|
b.Log.Errorln(message)
|
||||||
b.Log.Debugf("Verifying PermissionManageWebhooks for %s with ID %s", info.ID, id)
|
return fmt.Errorf("use of removed WebhookURL setting")
|
||||||
|
}
|
||||||
perms, permsErr := b.c.UserChannelPermissions(userinfo.ID, id)
|
|
||||||
if permsErr != nil {
|
if b.GetInt("debuglevel") == 2 {
|
||||||
b.Log.Warnf("Failed to check PermissionManageWebhooks in channel \"%s\": %s", info.Name, permsErr.Error())
|
b.Log.Debug("enabling even more discord debug")
|
||||||
} else if perms&manageWebhooks == manageWebhooks {
|
b.c.Debug = true
|
||||||
continue
|
}
|
||||||
}
|
|
||||||
channelsDenied = append(channelsDenied, fmt.Sprintf("%#v", info.Name))
|
// Initialise webhook management
|
||||||
}
|
b.transmitter = transmitter.New(b.c, b.guildID, "matterbridge", b.useAutoWebhooks)
|
||||||
|
b.transmitter.Log = b.Log
|
||||||
b.canEditWebhooks = len(channelsDenied) == 0
|
|
||||||
b.canEditWebhooks = false
|
var webhookChannelIDs []string
|
||||||
b.Log.Info("Webhook editing is disabled because of ratelimit issues")
|
for _, channel := range b.Channels {
|
||||||
/*
|
channelID := b.getChannelID(channel.Name) // note(qaisjp): this readlocks channelsMutex
|
||||||
if b.canEditWebhooks {
|
|
||||||
b.Log.Info("Can manage webhooks; will edit channel for global webhook on send")
|
// If a WebhookURL was not explicitly provided for this channel,
|
||||||
} else {
|
// there are two options: just a regular bot message (ugly) or this is should be webhook sent
|
||||||
b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send")
|
if channel.Options.WebhookURL == "" {
|
||||||
b.Log.Warn("Can't manage webhooks in channels: ", strings.Join(channelsDenied, ", "))
|
// If it should be webhook sent, we should enforce this via the transmitter
|
||||||
}
|
if b.useAutoWebhooks {
|
||||||
*/
|
webhookChannelIDs = append(webhookChannelIDs, channelID)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
b.channelsMutex.RUnlock()
|
|
||||||
|
|
||||||
// Obtaining guild members and initializing nickname mapping.
|
// Obtaining guild members and initializing nickname mapping.
|
||||||
b.membersMutex.Lock()
|
b.membersMutex.Lock()
|
||||||
@ -174,6 +222,19 @@ func (b *Bdiscord) Connect() error {
|
|||||||
b.nickMemberMap[member.Nick] = member
|
b.nickMemberMap[member.Nick] = member
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.c.AddHandler(b.messageCreate)
|
||||||
|
b.c.AddHandler(b.messageTyping)
|
||||||
|
b.c.AddHandler(b.messageUpdate)
|
||||||
|
b.c.AddHandler(b.messageDelete)
|
||||||
|
b.c.AddHandler(b.messageDeleteBulk)
|
||||||
|
b.c.AddHandler(b.memberAdd)
|
||||||
|
b.c.AddHandler(b.memberRemove)
|
||||||
|
b.c.AddHandler(b.memberUpdate)
|
||||||
|
if b.GetInt("debuglevel") == 1 {
|
||||||
|
b.c.AddHandler(b.messageEvent)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,80 +271,22 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
|
|||||||
msg.Text = "_" + msg.Text + "_"
|
msg.Text = "_" + msg.Text + "_"
|
||||||
}
|
}
|
||||||
|
|
||||||
// use initial webhook configured for the entire Discord account
|
// Handle prefix hint for unthreaded messages.
|
||||||
isGlobalWebhook := true
|
if msg.ParentNotFound() {
|
||||||
wID := b.webhookID
|
msg.ParentID = ""
|
||||||
wToken := b.webhookToken
|
|
||||||
|
|
||||||
// check if have a channel specific webhook
|
|
||||||
b.channelsMutex.RLock()
|
|
||||||
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
|
|
||||||
if ci.Options.WebhookURL != "" {
|
|
||||||
wID, wToken = b.splitURL(ci.Options.WebhookURL)
|
|
||||||
isGlobalWebhook = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
b.channelsMutex.RUnlock()
|
|
||||||
|
|
||||||
// Use webhook to send the message
|
// Use webhook to send the message
|
||||||
if wID != "" && msg.Event != config.EventMsgDelete {
|
useWebhooks := b.shouldMessageUseWebhooks(&msg)
|
||||||
// skip events
|
if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" {
|
||||||
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
|
return b.handleEventWebhook(&msg, channelID)
|
||||||
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)
|
|
||||||
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")
|
|
||||||
uri := discordgo.EndpointWebhookToken(wID, wToken) + "/messages/" + msg.ID
|
|
||||||
_, err := b.c.RequestWithBucketID("PATCH", uri, discordgo.WebhookParams{
|
|
||||||
Content: msg.Text,
|
|
||||||
Username: msg.Username,
|
|
||||||
}, discordgo.EndpointWebhookToken("", ""))
|
|
||||||
if err == nil {
|
|
||||||
return msg.ID, nil
|
|
||||||
}
|
|
||||||
b.Log.Errorf("Could not edit webhook message: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Log.Debugf("Broadcasting using Webhook")
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
if isGlobalWebhook && b.canEditWebhooks {
|
|
||||||
b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel)
|
|
||||||
_, err := b.c.WebhookEdit(wID, "", "", channelID)
|
|
||||||
if err != nil {
|
|
||||||
b.Log.Errorf("Could not set webhook channel: %s", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Log.Debugf("Processing webhook sending for message %#v", msg)
|
|
||||||
msg, err := b.webhookSend(&msg, wID, wToken)
|
|
||||||
if err != nil {
|
|
||||||
b.Log.Errorf("Could not broadcast via webook for message %#v: %s", msg, err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if msg == nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return msg.ID, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return b.handleEventBotUser(&msg, channelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleEventDirect handles events via the bot user
|
||||||
|
func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) {
|
||||||
b.Log.Debugf("Broadcasting using token (API)")
|
b.Log.Debugf("Broadcasting using token (API)")
|
||||||
|
|
||||||
// Delete message
|
// Delete message
|
||||||
@ -295,80 +298,87 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete a file
|
||||||
|
if msg.Event == config.EventFileDelete {
|
||||||
|
if msg.ID == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi, ok := b.cache.Get(cFileUpload + msg.ID); ok {
|
||||||
|
err := b.c.ChannelMessageDelete(channelID, fi.(string)) // nolint:forcetypeassert
|
||||||
|
b.cache.Remove(cFileUpload + msg.ID)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("file %s not found", msg.ID)
|
||||||
|
}
|
||||||
|
|
||||||
// Upload a file if it exists
|
// Upload a file if it exists
|
||||||
if msg.Extra != nil {
|
if msg.Extra != nil {
|
||||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
for _, rmsg := range helper.HandleExtra(msg, b.General) {
|
||||||
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength)
|
// TODO: Use ClipOrSplitMessage
|
||||||
|
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped"))
|
||||||
if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
|
if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
|
||||||
b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
|
b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check if we have files to upload (from slack, telegram or mattermost)
|
// check if we have files to upload (from slack, telegram or mattermost)
|
||||||
if len(msg.Extra["file"]) > 0 {
|
if len(msg.Extra["file"]) > 0 {
|
||||||
return b.handleUploadFile(&msg, channelID)
|
return b.handleUploadFile(msg, channelID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.Text = helper.ClipMessage(msg.Text, MessageLength)
|
|
||||||
msg.Text = b.replaceUserMentions(msg.Text)
|
|
||||||
|
|
||||||
// Edit message
|
// Edit message
|
||||||
if msg.ID != "" {
|
if msg.ID != "" {
|
||||||
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
|
// Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";".
|
||||||
return msg.ID, err
|
msgIds := strings.Split(msg.ID, ";")
|
||||||
}
|
msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds))
|
||||||
|
for len(msgParts) < len(msgIds) {
|
||||||
// Post normal message
|
msgParts = append(msgParts, "((obsoleted by edit))")
|
||||||
res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return res.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// useWebhook returns true if we have a webhook defined somewhere
|
|
||||||
func (b *Bdiscord) useWebhook() bool {
|
|
||||||
if b.GetString("WebhookURL") != "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
b.channelsMutex.RLock()
|
|
||||||
defer b.channelsMutex.RUnlock()
|
|
||||||
|
|
||||||
for _, channel := range b.channelInfoMap {
|
|
||||||
if channel.Options.WebhookURL != "" {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
for i := range msgParts {
|
||||||
return false
|
// In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates.
|
||||||
}
|
// TODO: Optimize away noop-updates of un-edited messages
|
||||||
|
// TODO: Use RemoteNickFormat instead of this broken concatenation
|
||||||
// isWebhookID returns true if the specified id is used in a defined webhook
|
_, err := b.c.ChannelMessageEdit(channelID, msgIds[i], msg.Username+msgParts[i])
|
||||||
func (b *Bdiscord) isWebhookID(id string) bool {
|
if err != nil {
|
||||||
if b.GetString("WebhookURL") != "" {
|
return "", err
|
||||||
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 msg.ID, nil
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), b.GetInt("MessageSplitMaxCount"))
|
||||||
|
msgIds := []string{}
|
||||||
|
|
||||||
|
for _, msgPart := range msgParts {
|
||||||
|
m := discordgo.MessageSend{
|
||||||
|
Content: msg.Username + msgPart,
|
||||||
|
AllowedMentions: b.getAllowedMentions(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.ParentValid() {
|
||||||
|
m.Reference = &discordgo.MessageReference{
|
||||||
|
MessageID: msg.ParentID,
|
||||||
|
ChannelID: channelID,
|
||||||
|
GuildID: b.guildID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post normal message
|
||||||
|
res, err := b.c.ChannelMessageSendComplex(channelID, &m)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
msgIds = append(msgIds, res.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exploit that a discord message ID is actually just a large number, so we encode a list of IDs by separating them with ";".
|
||||||
|
return strings.Join(msgIds, ";"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleUploadFile handles native upload of files
|
// handleUploadFile handles native upload of files
|
||||||
func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) {
|
func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) {
|
||||||
var err error
|
|
||||||
for _, f := range msg.Extra["file"] {
|
for _, f := range msg.Extra["file"] {
|
||||||
fi := f.(config.FileInfo)
|
fi := f.(config.FileInfo)
|
||||||
file := discordgo.File{
|
file := discordgo.File{
|
||||||
@ -377,93 +387,19 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
|
|||||||
Reader: bytes.NewReader(*fi.Data),
|
Reader: bytes.NewReader(*fi.Data),
|
||||||
}
|
}
|
||||||
m := discordgo.MessageSend{
|
m := discordgo.MessageSend{
|
||||||
Content: msg.Username + fi.Comment,
|
Content: msg.Username + fi.Comment,
|
||||||
Files: []*discordgo.File{&file},
|
Files: []*discordgo.File{&file},
|
||||||
|
AllowedMentions: b.getAllowedMentions(),
|
||||||
}
|
}
|
||||||
_, err = b.c.ChannelMessageSendComplex(channelID, &m)
|
res, err := b.c.ChannelMessageSendComplex(channelID, &m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("file upload failed: %s", err)
|
return "", fmt.Errorf("file upload failed: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// link file_upload_nativeID (file ID from the original bridge) to our upload id
|
||||||
|
// so that we can remove this later when it eg needs to be deleted
|
||||||
|
b.cache.Add(cFileUpload+fi.NativeID, res.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// webhookSend send one or more message via webhook, taking care of file
|
|
||||||
// uploads (from slack, telegram or mattermost).
|
|
||||||
// Returns messageID and error.
|
|
||||||
func (b *Bdiscord) webhookSend(msg *config.Message, webhookID, token string) (*discordgo.Message, error) {
|
|
||||||
var (
|
|
||||||
res *discordgo.Message
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
// If avatar is unset, check if UseLocalAvatar contains the message's
|
|
||||||
// account or protocol, and if so, try to find a local avatar
|
|
||||||
if msg.Avatar == "" {
|
|
||||||
for _, val := range b.GetStringSlice("UseLocalAvatar") {
|
|
||||||
if msg.Protocol == val || msg.Account == val {
|
|
||||||
if avatar := b.findAvatar(msg); avatar != "" {
|
|
||||||
msg.Avatar = avatar
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebhookParams can have either `Content` or `File`.
|
|
||||||
|
|
||||||
// We can't send empty messages.
|
|
||||||
if msg.Text != "" {
|
|
||||||
res, err = b.c.WebhookExecute(
|
|
||||||
webhookID,
|
|
||||||
token,
|
|
||||||
true,
|
|
||||||
&discordgo.WebhookParams{
|
|
||||||
Content: msg.Text,
|
|
||||||
Username: msg.Username,
|
|
||||||
AvatarURL: msg.Avatar,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.Extra != nil {
|
|
||||||
for _, f := range msg.Extra["file"] {
|
|
||||||
fi := f.(config.FileInfo)
|
|
||||||
file := discordgo.File{
|
|
||||||
Name: fi.Name,
|
|
||||||
ContentType: "",
|
|
||||||
Reader: bytes.NewReader(*fi.Data),
|
|
||||||
}
|
|
||||||
content := ""
|
|
||||||
if msg.Text == "" {
|
|
||||||
content = fi.Comment
|
|
||||||
}
|
|
||||||
_, e2 := b.c.WebhookExecute(
|
|
||||||
webhookID,
|
|
||||||
token,
|
|
||||||
false,
|
|
||||||
&discordgo.WebhookParams{
|
|
||||||
Username: msg.Username,
|
|
||||||
AvatarURL: msg.Avatar,
|
|
||||||
File: &file,
|
|
||||||
Content: content,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if e2 != nil {
|
|
||||||
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bdiscord) findAvatar(m *config.Message) string {
|
|
||||||
member, err := b.getGuildMemberByNick(m.Username)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return member.User.AvatarURL("")
|
|
||||||
}
|
|
||||||
|
@ -2,10 +2,15 @@ package bdiscord
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/matterbridge/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam
|
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 := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
|
||||||
rmsg.Channel = b.getChannelName(m.ChannelID)
|
rmsg.Channel = b.getChannelName(m.ChannelID)
|
||||||
|
|
||||||
@ -16,6 +21,10 @@ func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelet
|
|||||||
|
|
||||||
// TODO(qaisjp): if other bridges support bulk deletions, it could be fanned out centrally
|
// 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
|
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 {
|
for _, msgID := range m.Messages {
|
||||||
rmsg := config.Message{
|
rmsg := config.Message{
|
||||||
Account: b.Account,
|
Account: b.Account,
|
||||||
@ -31,7 +40,15 @@ func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bdiscord) messageEvent(s *discordgo.Session, m *discordgo.Event) {
|
||||||
|
b.Log.Debug(spew.Sdump(m.Struct))
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) {
|
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") {
|
if !b.GetBool("ShowUserTyping") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -47,11 +64,15 @@ func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam
|
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") {
|
if b.GetBool("EditDisable") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// only when message is actually edited
|
// only when message is actually edited
|
||||||
if m.Message.EditedTimestamp != "" {
|
if m.Message.EditedTimestamp != nil {
|
||||||
b.Log.Debugf("Sending edit message")
|
b.Log.Debugf("Sending edit message")
|
||||||
m.Content += b.GetString("EditSuffix")
|
m.Content += b.GetString("EditSuffix")
|
||||||
msg := &discordgo.MessageCreate{
|
msg := &discordgo.MessageCreate{
|
||||||
@ -62,6 +83,10 @@ func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdat
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam
|
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
|
var err error
|
||||||
|
|
||||||
// not relay our own messages
|
// not relay our own messages
|
||||||
@ -69,7 +94,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// if using webhooks, do not relay if it's ours
|
// if using webhooks, do not relay if it's ours
|
||||||
if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) {
|
if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,8 +107,9 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
|
|||||||
|
|
||||||
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
|
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
|
||||||
|
|
||||||
|
b.Log.Debugf("== Receiving event %#v", m.Message)
|
||||||
|
|
||||||
if m.Content != "" {
|
if m.Content != "" {
|
||||||
b.Log.Debugf("== Receiving event %#v", m.Message)
|
|
||||||
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
|
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
|
||||||
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
|
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -127,12 +153,21 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
|
|||||||
// Replace emotes
|
// Replace emotes
|
||||||
rmsg.Text = replaceEmotes(rmsg.Text)
|
rmsg.Text = replaceEmotes(rmsg.Text)
|
||||||
|
|
||||||
|
// Add our parent id if it exists, and if it's not referring to a message in another channel
|
||||||
|
if ref := m.MessageReference; ref != nil && ref.ChannelID == m.ChannelID {
|
||||||
|
rmsg.ParentID = ref.MessageID
|
||||||
|
}
|
||||||
|
|
||||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
|
||||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
b.Remote <- rmsg
|
b.Remote <- rmsg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
|
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 {
|
if m.Member == nil {
|
||||||
b.Log.Warnf("Received member update with no member information: %#v", m)
|
b.Log.Warnf("Received member update with no member information: %#v", m)
|
||||||
}
|
}
|
||||||
@ -160,6 +195,13 @@ func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUp
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
|
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 {
|
if m.Member == nil {
|
||||||
b.Log.Warnf("Received member update with no member information: %#v", m)
|
b.Log.Warnf("Received member update with no member information: %#v", m)
|
||||||
return
|
return
|
||||||
@ -181,6 +223,13 @@ func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
|
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 {
|
if m.Member == nil {
|
||||||
b.Log.Warnf("Received member update with no member information: %#v", m)
|
b.Log.Warnf("Received member update with no member information: %#v", m)
|
||||||
return
|
return
|
||||||
|
@ -3,7 +3,7 @@ package bdiscord
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/matterbridge/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -6,9 +6,33 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/matterbridge/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (b *Bdiscord) getAllowedMentions() *discordgo.MessageAllowedMentions {
|
||||||
|
// If AllowMention is not specified, then allow all mentions (default Discord behavior)
|
||||||
|
if !b.IsKeySet("AllowMention") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, allow only the mentions that are specified
|
||||||
|
allowedMentionTypes := make([]discordgo.AllowedMentionType, 0, 3)
|
||||||
|
for _, m := range b.GetStringSlice("AllowMention") {
|
||||||
|
switch m {
|
||||||
|
case "everyone":
|
||||||
|
allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeEveryone)
|
||||||
|
case "roles":
|
||||||
|
allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeRoles)
|
||||||
|
case "users":
|
||||||
|
allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeUsers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &discordgo.MessageAllowedMentions{
|
||||||
|
Parse: allowedMentionTypes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
|
func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
|
||||||
b.membersMutex.RLock()
|
b.membersMutex.RLock()
|
||||||
defer b.membersMutex.RUnlock()
|
defer b.membersMutex.RUnlock()
|
||||||
@ -44,7 +68,7 @@ func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error)
|
|||||||
b.membersMutex.RLock()
|
b.membersMutex.RLock()
|
||||||
defer b.membersMutex.RUnlock()
|
defer b.membersMutex.RUnlock()
|
||||||
|
|
||||||
if member, ok := b.nickMemberMap[nick]; ok {
|
if member, ok := b.nickMemberMap[strings.TrimSpace(nick)]; ok {
|
||||||
return member, nil
|
return member, nil
|
||||||
}
|
}
|
||||||
return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
|
return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
|
||||||
@ -196,7 +220,7 @@ func (b *Bdiscord) replaceAction(text string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// splitURL splits a webhookURL and returns the ID and token.
|
// splitURL splits a webhookURL and returns the ID and token.
|
||||||
func (b *Bdiscord) splitURL(url string) (string, string) {
|
func (b *Bdiscord) splitURL(url string) (string, string, bool) {
|
||||||
const (
|
const (
|
||||||
expectedWebhookSplitCount = 7
|
expectedWebhookSplitCount = 7
|
||||||
webhookIdxID = 5
|
webhookIdxID = 5
|
||||||
@ -204,9 +228,9 @@ func (b *Bdiscord) splitURL(url string) (string, string) {
|
|||||||
)
|
)
|
||||||
webhookURLSplit := strings.Split(url, "/")
|
webhookURLSplit := strings.Split(url, "/")
|
||||||
if len(webhookURLSplit) != expectedWebhookSplitCount {
|
if len(webhookURLSplit) != expectedWebhookSplitCount {
|
||||||
b.Log.Fatalf("%s is no correct discord WebhookURL", url)
|
return "", "", false
|
||||||
}
|
}
|
||||||
return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken]
|
return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken], true
|
||||||
}
|
}
|
||||||
|
|
||||||
func enumerateUsernames(s string) []string {
|
func enumerateUsernames(s string) []string {
|
||||||
|
257
bridge/discord/transmitter/transmitter.go
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
// Package transmitter provides functionality for transmitting
|
||||||
|
// arbitrary webhook messages to Discord.
|
||||||
|
//
|
||||||
|
// The package provides the following functionality:
|
||||||
|
//
|
||||||
|
// - Creating new webhooks, whenever necessary
|
||||||
|
// - Loading webhooks that we have previously created
|
||||||
|
// - Sending new messages
|
||||||
|
// - Editing messages, via message ID
|
||||||
|
// - Deleting messages, via message ID
|
||||||
|
//
|
||||||
|
// The package has been designed for matterbridge, but with other
|
||||||
|
// Go bots in mind. The public API should be matterbridge-agnostic.
|
||||||
|
package transmitter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Transmitter represents a message manager for a single guild.
|
||||||
|
type Transmitter struct {
|
||||||
|
session *discordgo.Session
|
||||||
|
guild string
|
||||||
|
title string
|
||||||
|
autoCreate bool
|
||||||
|
|
||||||
|
// channelWebhooks maps from a channel ID to a webhook instance
|
||||||
|
channelWebhooks map[string]*discordgo.Webhook
|
||||||
|
|
||||||
|
mutex sync.RWMutex
|
||||||
|
|
||||||
|
Log *log.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrWebhookNotFound is returned when a valid webhook for this channel/message combination does not exist
|
||||||
|
var ErrWebhookNotFound = errors.New("webhook for this channel and message does not exist")
|
||||||
|
|
||||||
|
// ErrPermissionDenied is returned if the bot does not have permission to manage webhooks.
|
||||||
|
//
|
||||||
|
// Bots can be granted a guild-wide permission and channel-specific permissions to manage webhooks.
|
||||||
|
// Despite potentially having guild-wide permission, channel specific overrides could deny a bot's permission to manage webhooks.
|
||||||
|
var ErrPermissionDenied = errors.New("missing 'Manage Webhooks' permission")
|
||||||
|
|
||||||
|
// New returns a new Transmitter given a Discord session, guild ID, and title.
|
||||||
|
func New(session *discordgo.Session, guild string, title string, autoCreate bool) *Transmitter {
|
||||||
|
return &Transmitter{
|
||||||
|
session: session,
|
||||||
|
guild: guild,
|
||||||
|
title: title,
|
||||||
|
autoCreate: autoCreate,
|
||||||
|
|
||||||
|
channelWebhooks: make(map[string]*discordgo.Webhook),
|
||||||
|
|
||||||
|
Log: log.NewEntry(log.StandardLogger()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send transmits a message to the given channel with the provided webhook data, and waits until Discord responds with message data.
|
||||||
|
func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) {
|
||||||
|
wh, err := t.getOrCreateWebhook(channelID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := t.session.WebhookExecute(wh.ID, wh.Token, true, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("execute failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit will edit a message in a channel, if possible.
|
||||||
|
func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo.WebhookParams) error {
|
||||||
|
wh := t.getWebhook(channelID)
|
||||||
|
|
||||||
|
if wh == nil {
|
||||||
|
return ErrWebhookNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := discordgo.EndpointWebhookToken(wh.ID, wh.Token) + "/messages/" + messageID
|
||||||
|
_, err := t.session.RequestWithBucketID("PATCH", uri, params, discordgo.EndpointWebhookToken("", ""))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasWebhook checks whether the transmitter is using a particular webhook.
|
||||||
|
func (t *Transmitter) HasWebhook(id string) bool {
|
||||||
|
t.mutex.RLock()
|
||||||
|
defer t.mutex.RUnlock()
|
||||||
|
|
||||||
|
for _, wh := range t.channelWebhooks {
|
||||||
|
if wh.ID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWebhook allows you to register a channel's webhook with the transmitter.
|
||||||
|
func (t *Transmitter) AddWebhook(channelID string, webhook *discordgo.Webhook) bool {
|
||||||
|
t.Log.Debugf("Manually added webhook %#v to channel %#v", webhook.ID, channelID)
|
||||||
|
t.mutex.Lock()
|
||||||
|
defer t.mutex.Unlock()
|
||||||
|
|
||||||
|
_, replaced := t.channelWebhooks[channelID]
|
||||||
|
t.channelWebhooks[channelID] = webhook
|
||||||
|
return replaced
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshGuildWebhooks loads "relevant" webhooks into the transmitter, with careful permission handling.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
//
|
||||||
|
// - A webhook is "relevant" if it was created by this bot -- the ApplicationID should match the bot's ID.
|
||||||
|
// - The term "having permission" means having the "Manage Webhooks" permission. See ErrPermissionDenied for more information.
|
||||||
|
// - This function is additive and will not unload previously loaded webhooks.
|
||||||
|
// - A nil channelIDs slice is treated the same as an empty one.
|
||||||
|
//
|
||||||
|
// If the bot has guild-wide permission:
|
||||||
|
//
|
||||||
|
// 1. it will load any "relevant" webhooks from the entire guild
|
||||||
|
// 2. the given slice is ignored
|
||||||
|
//
|
||||||
|
// If the bot does not have guild-wide permission:
|
||||||
|
//
|
||||||
|
// 1. it will load any "relevant" webhooks in each channel
|
||||||
|
// 2. a single error will be returned if any error occurs (incl. if there is no permission for any of these channels)
|
||||||
|
//
|
||||||
|
// If any channel has more than one "relevant" webhook, it will randomly pick one.
|
||||||
|
func (t *Transmitter) RefreshGuildWebhooks(channelIDs []string) error {
|
||||||
|
t.Log.Debugln("Refreshing guild webhooks")
|
||||||
|
|
||||||
|
botID, err := getDiscordUserID(t.session)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get current user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all existing webhooks
|
||||||
|
hooks, err := t.session.GuildWebhooks(t.guild)
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case isDiscordPermissionError(err):
|
||||||
|
// We fallback on manually fetching hooks from individual channels
|
||||||
|
// if we don't have the "Manage Webhooks" permission globally.
|
||||||
|
// We can only do this if we were provided channelIDs, though.
|
||||||
|
if len(channelIDs) == 0 {
|
||||||
|
return ErrPermissionDenied
|
||||||
|
}
|
||||||
|
t.Log.Debugln("Missing global 'Manage Webhooks' permission, falling back on per-channel permission")
|
||||||
|
return t.fetchChannelsHooks(channelIDs, botID)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("could not get webhooks: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log.Debugln("Refreshing guild webhooks using global permission")
|
||||||
|
t.assignHooksByAppID(hooks, botID, false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createWebhook creates a webhook for a specific channel.
|
||||||
|
func (t *Transmitter) createWebhook(channel string) (*discordgo.Webhook, error) {
|
||||||
|
t.mutex.Lock()
|
||||||
|
defer t.mutex.Unlock()
|
||||||
|
|
||||||
|
wh, err := t.session.WebhookCreate(channel, t.title+time.Now().Format(" 3:04:05PM"), "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.channelWebhooks[channel] = wh
|
||||||
|
return wh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmitter) getWebhook(channel string) *discordgo.Webhook {
|
||||||
|
t.mutex.RLock()
|
||||||
|
defer t.mutex.RUnlock()
|
||||||
|
|
||||||
|
return t.channelWebhooks[channel]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmitter) getOrCreateWebhook(channelID string) (*discordgo.Webhook, error) {
|
||||||
|
// If we have a webhook for this channel, immediately return it
|
||||||
|
wh := t.getWebhook(channelID)
|
||||||
|
if wh != nil {
|
||||||
|
return wh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early exit if we don't want to automatically create one
|
||||||
|
if !t.autoCreate {
|
||||||
|
return nil, ErrWebhookNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log.Infof("Creating a webhook for %s\n", channelID)
|
||||||
|
wh, err := t.createWebhook(channelID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create webhook: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchChannelsHooks fetches hooks for the given channelIDs and calls assignHooksByAppID for each channel's hooks
|
||||||
|
func (t *Transmitter) fetchChannelsHooks(channelIDs []string, botID string) error {
|
||||||
|
// For each channel, search for relevant hooks
|
||||||
|
var failedHooks []string
|
||||||
|
for _, channelID := range channelIDs {
|
||||||
|
hooks, err := t.session.ChannelWebhooks(channelID)
|
||||||
|
if err != nil {
|
||||||
|
failedHooks = append(failedHooks, "\n- "+channelID+": "+err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.assignHooksByAppID(hooks, botID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose an error if any hooks failed
|
||||||
|
if len(failedHooks) > 0 {
|
||||||
|
return errors.New("failed to fetch hooks:" + strings.Join(failedHooks, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmitter) assignHooksByAppID(hooks []*discordgo.Webhook, appID string, channelTargeted bool) {
|
||||||
|
logLine := "Picking up webhook"
|
||||||
|
if channelTargeted {
|
||||||
|
logLine += " (channel targeted)"
|
||||||
|
}
|
||||||
|
|
||||||
|
t.mutex.Lock()
|
||||||
|
defer t.mutex.Unlock()
|
||||||
|
|
||||||
|
for _, wh := range hooks {
|
||||||
|
if wh.ApplicationID != appID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.channelWebhooks[wh.ChannelID] = wh
|
||||||
|
t.Log.WithFields(log.Fields{
|
||||||
|
"id": wh.ID,
|
||||||
|
"name": wh.Name,
|
||||||
|
"channel": wh.ChannelID,
|
||||||
|
}).Println(logLine)
|
||||||
|
}
|
||||||
|
}
|
32
bridge/discord/transmitter/utils.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package transmitter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions
|
||||||
|
func isDiscordPermissionError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
restErr, ok := err.(*discordgo.RESTError)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return restErr.Message != nil && restErr.Message.Code == discordgo.ErrCodeMissingPermissions
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDiscordUserID gets own user ID from state, and fallback on API request
|
||||||
|
func getDiscordUserID(session *discordgo.Session) (string, error) {
|
||||||
|
if user := session.State.User; user != nil {
|
||||||
|
return user.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := session.User("@me")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return user.ID, nil
|
||||||
|
}
|
179
bridge/discord/webhook.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package bdiscord
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks
|
||||||
|
func (b *Bdiscord) shouldMessageUseWebhooks(msg *config.Message) bool {
|
||||||
|
if b.useAutoWebhooks {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
b.channelsMutex.RLock()
|
||||||
|
defer b.channelsMutex.RUnlock()
|
||||||
|
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
|
||||||
|
if ci.Options.WebhookURL != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeGetLocalAvatar checks if UseLocalAvatar contains the message's
|
||||||
|
// account or protocol, and if so, returns the Discord avatar (if exists)
|
||||||
|
func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
|
||||||
|
for _, val := range b.GetStringSlice("UseLocalAvatar") {
|
||||||
|
if msg.Protocol != val && msg.Account != val {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
member, err := b.getGuildMemberByNick(msg.Username)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return member.User.AvatarURL("")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bdiscord) webhookSendTextOnly(msg *config.Message, channelID string) (string, error) {
|
||||||
|
msgParts := helper.ClipOrSplitMessage(msg.Text, MessageLength, b.GetString("MessageClipped"), b.GetInt("MessageSplitMaxCount"))
|
||||||
|
msgIds := []string{}
|
||||||
|
for _, msgPart := range msgParts {
|
||||||
|
res, err := b.transmitter.Send(
|
||||||
|
channelID,
|
||||||
|
&discordgo.WebhookParams{
|
||||||
|
Content: msgPart,
|
||||||
|
Username: msg.Username,
|
||||||
|
AvatarURL: msg.Avatar,
|
||||||
|
AllowedMentions: b.getAllowedMentions(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
msgIds = append(msgIds, res.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Exploit that a discord message ID is actually just a large number, so we encode a list of IDs by separating them with ";".
|
||||||
|
return strings.Join(msgIds, ";"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bdiscord) webhookSendFilesOnly(msg *config.Message, channelID string) error {
|
||||||
|
for _, f := range msg.Extra["file"] {
|
||||||
|
fi := f.(config.FileInfo) //nolint:forcetypeassert
|
||||||
|
file := discordgo.File{
|
||||||
|
Name: fi.Name,
|
||||||
|
ContentType: "",
|
||||||
|
Reader: bytes.NewReader(*fi.Data),
|
||||||
|
}
|
||||||
|
content := fi.Comment
|
||||||
|
|
||||||
|
// Cannot use the resulting ID for any edits anyway, so throw it away.
|
||||||
|
// This has to be re-enabled when we implement message deletion.
|
||||||
|
_, err := b.transmitter.Send(
|
||||||
|
channelID,
|
||||||
|
&discordgo.WebhookParams{
|
||||||
|
Username: msg.Username,
|
||||||
|
AvatarURL: msg.Avatar,
|
||||||
|
Files: []*discordgo.File{&file},
|
||||||
|
Content: content,
|
||||||
|
AllowedMentions: b.getAllowedMentions(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// webhookSend send one or more message via webhook, taking care of file
|
||||||
|
// uploads (from slack, telegram or mattermost).
|
||||||
|
// Returns messageID and error.
|
||||||
|
func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (string, error) {
|
||||||
|
var (
|
||||||
|
res string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this)
|
||||||
|
if msg.Avatar == "" {
|
||||||
|
msg.Avatar = b.maybeGetLocalAvatar(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookParams can have either `Content` or `File`.
|
||||||
|
|
||||||
|
// We can't send empty messages.
|
||||||
|
if msg.Text != "" {
|
||||||
|
res, err = b.webhookSendTextOnly(msg, channelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && msg.Extra != nil {
|
||||||
|
err = b.webhookSendFilesOnly(msg, channelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (string, error) {
|
||||||
|
// skip events
|
||||||
|
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip empty messages
|
||||||
|
if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) {
|
||||||
|
b.Log.Debugf("Skipping empty message %#v", msg)
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// discord username must be [0..32] max
|
||||||
|
if len(msg.Username) > 32 {
|
||||||
|
msg.Username = msg.Username[0:32]
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.ID != "" {
|
||||||
|
// Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";".
|
||||||
|
msgIds := strings.Split(msg.ID, ";")
|
||||||
|
msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds))
|
||||||
|
for len(msgParts) < len(msgIds) {
|
||||||
|
msgParts = append(msgParts, "((obsoleted by edit))")
|
||||||
|
}
|
||||||
|
b.Log.Debugf("Editing webhook message")
|
||||||
|
var editErr error = nil
|
||||||
|
for i := range msgParts {
|
||||||
|
// In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates.
|
||||||
|
// TODO: Optimize away noop-updates of un-edited messages
|
||||||
|
editErr = b.transmitter.Edit(channelID, msgIds[i], &discordgo.WebhookParams{
|
||||||
|
Content: msgParts[i],
|
||||||
|
Username: msg.Username,
|
||||||
|
AllowedMentions: b.getAllowedMentions(),
|
||||||
|
})
|
||||||
|
if editErr != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if editErr == nil {
|
||||||
|
return msg.ID, nil
|
||||||
|
}
|
||||||
|
b.Log.Errorf("Could not edit webhook message(s): %s; sending as new message(s) instead", editErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugf("Processing webhook sending for message %#v", msg)
|
||||||
|
msg.Text = b.replaceUserMentions(msg.Text)
|
||||||
|
msgID, err := b.webhookSend(msg, channelID)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msgID, err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return msgID, nil
|
||||||
|
}
|
@ -1,182 +0,0 @@
|
|||||||
package bgitter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/42wim/go-gitter"
|
|
||||||
"github.com/42wim/matterbridge/bridge"
|
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Bgitter struct {
|
|
||||||
c *gitter.Gitter
|
|
||||||
User *gitter.User
|
|
||||||
Users []gitter.User
|
|
||||||
Rooms []gitter.Room
|
|
||||||
*bridge.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(cfg *bridge.Config) bridge.Bridger {
|
|
||||||
return &Bgitter{Config: cfg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bgitter) Connect() error {
|
|
||||||
var err error
|
|
||||||
b.Log.Info("Connecting")
|
|
||||||
b.c = gitter.New(b.GetString("Token"))
|
|
||||||
b.User, err = b.c.GetUser()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.Rooms, err = b.c.GetRooms()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.Log.Info("Connection succeeded")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bgitter) Disconnect() error {
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
|
|
||||||
roomID, err := b.c.GetRoomId(channel.Name)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Could not find roomID for %v. Please create the room on gitter.im", channel.Name)
|
|
||||||
}
|
|
||||||
room, err := b.c.GetRoom(roomID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.Rooms = append(b.Rooms, *room)
|
|
||||||
user, err := b.c.GetUser()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = b.c.JoinRoom(roomID, user.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
users, _ := b.c.GetUsersInRoom(roomID)
|
|
||||||
b.Users = append(b.Users, users...)
|
|
||||||
stream := b.c.Stream(roomID)
|
|
||||||
go b.c.Listen(stream)
|
|
||||||
|
|
||||||
go func(stream *gitter.Stream, room string) {
|
|
||||||
for event := range stream.Event {
|
|
||||||
switch ev := event.Data.(type) {
|
|
||||||
case *gitter.MessageReceived:
|
|
||||||
// ignore message sent from ourselves
|
|
||||||
if ev.Message.From.ID != b.User.ID {
|
|
||||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
|
|
||||||
rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
|
|
||||||
Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID,
|
|
||||||
ID: ev.Message.ID}
|
|
||||||
if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) {
|
|
||||||
rmsg.Event = config.EventUserAction
|
|
||||||
rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1)
|
|
||||||
}
|
|
||||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
|
||||||
b.Remote <- rmsg
|
|
||||||
}
|
|
||||||
case *gitter.GitterConnectionClosed:
|
|
||||||
b.Log.Errorf("connection with gitter closed for room %s", room)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(stream, room.URI)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bgitter) Send(msg config.Message) (string, error) {
|
|
||||||
b.Log.Debugf("=> Receiving %#v", msg)
|
|
||||||
roomID := b.getRoomID(msg.Channel)
|
|
||||||
if roomID == "" {
|
|
||||||
b.Log.Errorf("Could not find roomID for %v", msg.Channel)
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete message
|
|
||||||
if msg.Event == config.EventMsgDelete {
|
|
||||||
if msg.ID == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
// gitter has no delete message api so we edit message to ""
|
|
||||||
_, err := b.c.UpdateMessage(roomID, msg.ID, "")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload a file (in gitter case send the upload URL because gitter has no native upload support)
|
|
||||||
if msg.Extra != nil {
|
|
||||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
|
||||||
b.c.SendMessage(roomID, rmsg.Username+rmsg.Text)
|
|
||||||
}
|
|
||||||
if len(msg.Extra["file"]) > 0 {
|
|
||||||
return b.handleUploadFile(&msg, roomID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit message
|
|
||||||
if msg.ID != "" {
|
|
||||||
b.Log.Debugf("updating message with id %s", msg.ID)
|
|
||||||
_, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post normal message
|
|
||||||
resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return resp.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bgitter) getRoomID(channel string) string {
|
|
||||||
for _, v := range b.Rooms {
|
|
||||||
if v.URI == channel {
|
|
||||||
return v.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bgitter) getAvatar(user string) string {
|
|
||||||
var avatar string
|
|
||||||
if b.Users != nil {
|
|
||||||
for _, u := range b.Users {
|
|
||||||
if user == u.Username {
|
|
||||||
return u.AvatarURLSmall
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return avatar
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bgitter) handleUploadFile(msg *config.Message, roomID string) (string, error) {
|
|
||||||
for _, f := range msg.Extra["file"] {
|
|
||||||
fi := f.(config.FileInfo)
|
|
||||||
if fi.Comment != "" {
|
|
||||||
msg.Text += fi.Comment + ": "
|
|
||||||
}
|
|
||||||
if fi.URL != "" {
|
|
||||||
msg.Text = fi.URL
|
|
||||||
if fi.Comment != "" {
|
|
||||||
msg.Text = fi.Comment + ": " + fi.URL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
252
bridge/harmony/harmony.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
package harmony
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/42wim/matterbridge/bridge"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/harmony-development/shibshib"
|
||||||
|
chatv1 "github.com/harmony-development/shibshib/gen/chat/v1"
|
||||||
|
typesv1 "github.com/harmony-development/shibshib/gen/harmonytypes/v1"
|
||||||
|
profilev1 "github.com/harmony-development/shibshib/gen/profile/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cachedProfile struct {
|
||||||
|
data *profilev1.GetProfileResponse
|
||||||
|
lastUpdated time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bharmony struct {
|
||||||
|
*bridge.Config
|
||||||
|
|
||||||
|
c *shibshib.Client
|
||||||
|
profileCache map[uint64]cachedProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
func uToStr(in uint64) string {
|
||||||
|
return strconv.FormatUint(in, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func strToU(in string) (uint64, error) {
|
||||||
|
return strconv.ParseUint(in, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg *bridge.Config) bridge.Bridger {
|
||||||
|
b := &Bharmony{
|
||||||
|
Config: cfg,
|
||||||
|
profileCache: map[uint64]cachedProfile{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) getProfile(u uint64) (*profilev1.GetProfileResponse, error) {
|
||||||
|
if v, ok := b.profileCache[u]; ok && time.Since(v.lastUpdated) < time.Minute*10 {
|
||||||
|
return v.data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := b.c.ProfileKit.GetProfile(&profilev1.GetProfileRequest{
|
||||||
|
UserId: u,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if v, ok := b.profileCache[u]; ok {
|
||||||
|
return v.data, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.profileCache[u] = cachedProfile{
|
||||||
|
data: resp,
|
||||||
|
lastUpdated: time.Now(),
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) avatarFor(m *chatv1.Message) string {
|
||||||
|
if m.Overrides != nil {
|
||||||
|
return m.Overrides.GetAvatar()
|
||||||
|
}
|
||||||
|
|
||||||
|
profi, err := b.getProfile(m.AuthorId)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.c.TransformHMCURL(profi.Profile.GetUserAvatar())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) usernameFor(m *chatv1.Message) string {
|
||||||
|
if m.Overrides != nil {
|
||||||
|
return m.Overrides.GetUsername()
|
||||||
|
}
|
||||||
|
|
||||||
|
profi, err := b.getProfile(m.AuthorId)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return profi.Profile.UserName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) toMessage(msg *shibshib.LocatedMessage) config.Message {
|
||||||
|
message := config.Message{}
|
||||||
|
message.Account = b.Account
|
||||||
|
message.UserID = uToStr(msg.Message.AuthorId)
|
||||||
|
message.Avatar = b.avatarFor(msg.Message)
|
||||||
|
message.Username = b.usernameFor(msg.Message)
|
||||||
|
message.Channel = uToStr(msg.ChannelID)
|
||||||
|
message.ID = uToStr(msg.MessageId)
|
||||||
|
|
||||||
|
switch content := msg.Message.Content.Content.(type) {
|
||||||
|
case *chatv1.Content_EmbedMessage:
|
||||||
|
message.Text = "Embed"
|
||||||
|
case *chatv1.Content_AttachmentMessage:
|
||||||
|
var s strings.Builder
|
||||||
|
for idx, attach := range content.AttachmentMessage.Files {
|
||||||
|
s.WriteString(b.c.TransformHMCURL(attach.Id))
|
||||||
|
if idx < len(content.AttachmentMessage.Files)-1 {
|
||||||
|
s.WriteString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message.Text = s.String()
|
||||||
|
case *chatv1.Content_PhotoMessage:
|
||||||
|
var s strings.Builder
|
||||||
|
for idx, attach := range content.PhotoMessage.GetPhotos() {
|
||||||
|
s.WriteString(attach.GetCaption().GetText())
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(b.c.TransformHMCURL(attach.GetHmc()))
|
||||||
|
if idx < len(content.PhotoMessage.GetPhotos())-1 {
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message.Text = s.String()
|
||||||
|
case *chatv1.Content_TextMessage:
|
||||||
|
message.Text = content.TextMessage.Content.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) outputMessages() {
|
||||||
|
for {
|
||||||
|
msg := <-b.c.EventsStream()
|
||||||
|
|
||||||
|
if msg.Message.AuthorId == b.c.UserID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Remote <- b.toMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) GetUint64(conf string) uint64 {
|
||||||
|
num, err := strToU(b.GetString(conf))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) Connect() (err error) {
|
||||||
|
b.c, err = shibshib.NewClient(b.GetString("Homeserver"), b.GetString("Token"), b.GetUint64("UserID"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.c.SubscribeToGuild(b.GetUint64("Community"))
|
||||||
|
|
||||||
|
go b.outputMessages()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) send(msg config.Message) (id string, err error) {
|
||||||
|
msgChan, err := strToU(msg.Channel)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
retID, err := b.c.ChatKit.SendMessage(&chatv1.SendMessageRequest{
|
||||||
|
GuildId: b.GetUint64("Community"),
|
||||||
|
ChannelId: msgChan,
|
||||||
|
Content: &chatv1.Content{
|
||||||
|
Content: &chatv1.Content_TextMessage{
|
||||||
|
TextMessage: &chatv1.Content_TextContent{
|
||||||
|
Content: &chatv1.FormattedText{
|
||||||
|
Text: msg.Text,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Overrides: &chatv1.Overrides{
|
||||||
|
Username: &msg.Username,
|
||||||
|
Avatar: &msg.Avatar,
|
||||||
|
Reason: &chatv1.Overrides_Bridge{Bridge: &typesv1.Empty{}},
|
||||||
|
},
|
||||||
|
InReplyTo: nil,
|
||||||
|
EchoId: nil,
|
||||||
|
Metadata: nil,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("send: error sending message: %w", err)
|
||||||
|
log.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return uToStr(retID.MessageId), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) delete(msg config.Message) (id string, err error) {
|
||||||
|
msgChan, err := strToU(msg.Channel)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
msgID, err := strToU(msg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = b.c.ChatKit.DeleteMessage(&chatv1.DeleteMessageRequest{
|
||||||
|
GuildId: b.GetUint64("Community"),
|
||||||
|
ChannelId: msgChan,
|
||||||
|
MessageId: msgID,
|
||||||
|
})
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) typing(msg config.Message) (id string, err error) {
|
||||||
|
msgChan, err := strToU(msg.Channel)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = b.c.ChatKit.Typing(&chatv1.TypingRequest{
|
||||||
|
GuildId: b.GetUint64("Community"),
|
||||||
|
ChannelId: msgChan,
|
||||||
|
})
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) Send(msg config.Message) (id string, err error) {
|
||||||
|
switch msg.Event {
|
||||||
|
case "":
|
||||||
|
return b.send(msg)
|
||||||
|
case config.EventMsgDelete:
|
||||||
|
return b.delete(msg)
|
||||||
|
case config.EventUserTyping:
|
||||||
|
return b.typing(msg)
|
||||||
|
default:
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bharmony) Disconnect() error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -5,10 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -51,6 +48,30 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) {
|
|||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloadFileAuthRocket downloads the given URL using the specified Rocket user ID and authentication token.
|
||||||
|
func DownloadFileAuthRocket(url, token, userID string) (*[]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 5,
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
|
||||||
|
req.Header.Add("X-Auth-Token", token)
|
||||||
|
req.Header.Add("X-User-Id", userID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
_, err = io.Copy(&buf, resp.Body)
|
||||||
|
data := buf.Bytes()
|
||||||
|
return &data, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetSubLines splits messages in newline-delimited lines. If maxLineLength is
|
// GetSubLines splits messages in newline-delimited lines. If maxLineLength is
|
||||||
// specified as non-zero GetSubLines will also clip long lines to the maximum
|
// specified as non-zero GetSubLines will also clip long lines to the maximum
|
||||||
// length and insert a warning marker that the line was clipped.
|
// length and insert a warning marker that the line was clipped.
|
||||||
@ -58,11 +79,19 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) {
|
|||||||
// TODO: The current implementation has the inconvenient that it disregards
|
// TODO: The current implementation has the inconvenient that it disregards
|
||||||
// word boundaries when splitting but this is hard to solve without potentially
|
// word boundaries when splitting but this is hard to solve without potentially
|
||||||
// breaking formatting and other stylistic effects.
|
// breaking formatting and other stylistic effects.
|
||||||
func GetSubLines(message string, maxLineLength int) []string {
|
func GetSubLines(message string, maxLineLength int, clippingMessage string) []string {
|
||||||
const clippingMessage = " <clipped message>"
|
if clippingMessage == "" {
|
||||||
|
clippingMessage = " <clipped message>"
|
||||||
|
}
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
for _, line := range strings.Split(strings.TrimSpace(message), "\n") {
|
for _, line := range strings.Split(strings.TrimSpace(message), "\n") {
|
||||||
|
if line == "" {
|
||||||
|
// Prevent sending empty messages, so we'll skip this line
|
||||||
|
// if it has no content.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if maxLineLength == 0 || len([]byte(line)) <= maxLineLength {
|
if maxLineLength == 0 || len([]byte(line)) <= maxLineLength {
|
||||||
lines = append(lines, line)
|
lines = append(lines, line)
|
||||||
continue
|
continue
|
||||||
@ -145,17 +174,23 @@ func HandleDownloadSize(logger *logrus.Entry, msg *config.Message, name string,
|
|||||||
|
|
||||||
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
|
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
|
||||||
func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
|
func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
|
||||||
|
HandleDownloadData2(logger, msg, name, "", comment, url, data, general)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
|
||||||
|
func HandleDownloadData2(logger *logrus.Entry, msg *config.Message, name, id, comment, url string, data *[]byte, general *config.Protocol) {
|
||||||
var avatar bool
|
var avatar bool
|
||||||
logger.Debugf("Download OK %#v %#v", name, len(*data))
|
logger.Debugf("Download OK %#v %#v", name, len(*data))
|
||||||
if msg.Event == config.EventAvatarDownload {
|
if msg.Event == config.EventAvatarDownload {
|
||||||
avatar = true
|
avatar = true
|
||||||
}
|
}
|
||||||
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{
|
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{
|
||||||
Name: name,
|
Name: name,
|
||||||
Data: data,
|
Data: data,
|
||||||
URL: url,
|
URL: url,
|
||||||
Comment: comment,
|
Comment: comment,
|
||||||
Avatar: avatar,
|
Avatar: avatar,
|
||||||
|
NativeID: id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,21 +204,61 @@ func RemoveEmptyNewLines(msg string) string {
|
|||||||
|
|
||||||
// ClipMessage trims a message to the specified length if it exceeds it and adds a warning
|
// ClipMessage trims a message to the specified length if it exceeds it and adds a warning
|
||||||
// to the message in case it does so.
|
// to the message in case it does so.
|
||||||
func ClipMessage(text string, length int) string {
|
func ClipMessage(text string, length int, clippingMessage string) string {
|
||||||
const clippingMessage = " <clipped message>"
|
if clippingMessage == "" {
|
||||||
|
clippingMessage = " <clipped message>"
|
||||||
|
}
|
||||||
|
|
||||||
if len(text) > length {
|
if len(text) > length {
|
||||||
text = text[:length-len(clippingMessage)]
|
text = text[:length-len(clippingMessage)]
|
||||||
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
|
for len(text) > 0 {
|
||||||
text = text[:len(text)-size]
|
if r, _ := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
|
||||||
|
text = text[:len(text)-1]
|
||||||
|
// Note: DecodeLastRuneInString only returns the constant value "1" in
|
||||||
|
// case of an error. We do not yet know whether the last rune is now
|
||||||
|
// actually valid. Example: "€" is 0xE2 0x82 0xAC. If we happen to split
|
||||||
|
// the string just before 0xAC, and go back only one byte, that would
|
||||||
|
// leave us with a string that ends in the byte 0xE2, which is not a valid
|
||||||
|
// rune, so we need to try again.
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
text += clippingMessage
|
text += clippingMessage
|
||||||
}
|
}
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ClipOrSplitMessage(text string, length int, clippingMessage string, splitMax int) []string {
|
||||||
|
var msgParts []string
|
||||||
|
remainingText := text
|
||||||
|
// Invariant of this splitting loop: No text is lost (msgParts+remainingText is the original text),
|
||||||
|
// and all parts is guaranteed to satisfy the length requirement.
|
||||||
|
for len(msgParts) < splitMax-1 && len(remainingText) > length {
|
||||||
|
// Decision: The text needs to be split (again).
|
||||||
|
var chunk string
|
||||||
|
wasted := 0
|
||||||
|
// The longest UTF-8 encoding of a valid rune is 4 bytes (0xF4 0x8F 0xBF 0xBF, encoding U+10FFFF),
|
||||||
|
// so we should never need to waste 4 or more bytes at a time.
|
||||||
|
for wasted < 4 && wasted < length {
|
||||||
|
chunk = remainingText[:length-wasted]
|
||||||
|
if r, _ := utf8.DecodeLastRuneInString(chunk); r == utf8.RuneError {
|
||||||
|
wasted += 1
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Note: At this point, "chunk" might still be invalid, if "text" is very broken.
|
||||||
|
msgParts = append(msgParts, chunk)
|
||||||
|
remainingText = remainingText[len(chunk):]
|
||||||
|
}
|
||||||
|
msgParts = append(msgParts, ClipMessage(remainingText, length, clippingMessage))
|
||||||
|
return msgParts
|
||||||
|
}
|
||||||
|
|
||||||
// ParseMarkdown takes in an input string as markdown and parses it to html
|
// ParseMarkdown takes in an input string as markdown and parses it to html
|
||||||
func ParseMarkdown(input string) string {
|
func ParseMarkdown(input string) string {
|
||||||
extensions := parser.HardLineBreak | parser.NoIntraEmphasis
|
extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode
|
||||||
markdownParser := parser.NewWithExtensions(extensions)
|
markdownParser := parser.NewWithExtensions(extensions)
|
||||||
renderer := html.NewRenderer(html.RendererOptions{
|
renderer := html.NewRenderer(html.RendererOptions{
|
||||||
Flags: 0,
|
Flags: 0,
|
||||||
@ -210,49 +285,3 @@ func ConvertWebPToPNG(data *[]byte) error {
|
|||||||
*data = w.Bytes()
|
*data = w.Bytes()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanConvertTgsToX Checks whether the external command necessary for ConvertTgsToX works.
|
|
||||||
func CanConvertTgsToX() error {
|
|
||||||
// We depend on the fact that `lottie_convert.py --help` has exit status 0.
|
|
||||||
// Hyrum's Law predicted this, and Murphy's Law predicts that this will break eventually.
|
|
||||||
// However, there is no alternative like `lottie_convert.py --is-properly-installed`
|
|
||||||
cmd := exec.Command("lottie_convert.py", "--help")
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertTgsToWebP convert input data (which should be tgs format) to WebP format
|
|
||||||
// This relies on an external command, which is ugly, but works.
|
|
||||||
func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
|
|
||||||
// lottie can't handle input from a pipe, so write to a temporary file:
|
|
||||||
tmpFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-*.tgs")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmpFileName := tmpFile.Name()
|
|
||||||
defer func() {
|
|
||||||
if removeErr := os.Remove(tmpFileName); removeErr != nil {
|
|
||||||
logger.Errorf("Could not delete temporary file %s: %v", tmpFileName, removeErr)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if _, writeErr := tmpFile.Write(*data); writeErr != nil {
|
|
||||||
return writeErr
|
|
||||||
}
|
|
||||||
// Must close before calling lottie to avoid data races:
|
|
||||||
if closeErr := tmpFile.Close(); closeErr != nil {
|
|
||||||
return closeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call lottie to transform:
|
|
||||||
cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpFileName, "/dev/stdout")
|
|
||||||
cmd.Stderr = nil
|
|
||||||
// NB: lottie writes progress into to stderr in all cases.
|
|
||||||
stdout, stderr := cmd.Output()
|
|
||||||
if stderr != nil {
|
|
||||||
// 'stderr' already contains some parts of Stderr, because it was set to 'nil'.
|
|
||||||
return stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
*data = stdout
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -10,98 +10,105 @@ import (
|
|||||||
|
|
||||||
const testLineLength = 64
|
const testLineLength = 64
|
||||||
|
|
||||||
var (
|
var lineSplittingTestCases = map[string]struct {
|
||||||
lineSplittingTestCases = map[string]struct {
|
input string
|
||||||
input string
|
splitOutput []string
|
||||||
splitOutput []string
|
nonSplitOutput []string
|
||||||
nonSplitOutput []string
|
}{
|
||||||
}{
|
"Short single-line message": {
|
||||||
"Short single-line message": {
|
input: "short",
|
||||||
input: "short",
|
splitOutput: []string{"short"},
|
||||||
splitOutput: []string{"short"},
|
nonSplitOutput: []string{"short"},
|
||||||
nonSplitOutput: []string{"short"},
|
},
|
||||||
|
"Long single-line message": {
|
||||||
|
input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||||
|
splitOutput: []string{
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
|
||||||
|
"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
|
||||||
|
" labore et dolore magna aliqua.",
|
||||||
},
|
},
|
||||||
"Long single-line message": {
|
nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."},
|
||||||
input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
},
|
||||||
splitOutput: []string{
|
"Short multi-line message": {
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
|
input: "I\ncan't\nget\nno\nsatisfaction!",
|
||||||
"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
|
splitOutput: []string{
|
||||||
" labore et dolore magna aliqua.",
|
"I",
|
||||||
},
|
"can't",
|
||||||
nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."},
|
"get",
|
||||||
|
"no",
|
||||||
|
"satisfaction!",
|
||||||
},
|
},
|
||||||
"Short multi-line message": {
|
nonSplitOutput: []string{
|
||||||
input: "I\ncan't\nget\nno\nsatisfaction!",
|
"I",
|
||||||
splitOutput: []string{
|
"can't",
|
||||||
"I",
|
"get",
|
||||||
"can't",
|
"no",
|
||||||
"get",
|
"satisfaction!",
|
||||||
"no",
|
|
||||||
"satisfaction!",
|
|
||||||
},
|
|
||||||
nonSplitOutput: []string{
|
|
||||||
"I",
|
|
||||||
"can't",
|
|
||||||
"get",
|
|
||||||
"no",
|
|
||||||
"satisfaction!",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"Long multi-line message": {
|
},
|
||||||
input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
|
"Long multi-line message": {
|
||||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
|
input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
|
||||||
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" +
|
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
|
||||||
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" +
|
||||||
splitOutput: []string{
|
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
|
splitOutput: []string{
|
||||||
"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
|
"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
|
||||||
" labore et dolore magna aliqua.",
|
"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
|
||||||
"Ut enim ad minim veniam, quis nostrud exercita <clipped message>",
|
" labore et dolore magna aliqua.",
|
||||||
"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>",
|
"Ut enim ad minim veniam, quis nostrud exercita <clipped message>",
|
||||||
"modo consequat.",
|
"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>",
|
||||||
"Duis aute irure dolor in reprehenderit in volu <clipped message>",
|
"modo consequat.",
|
||||||
"ptate velit esse cillum dolore eu fugiat nulla <clipped message>",
|
"Duis aute irure dolor in reprehenderit in volu <clipped message>",
|
||||||
" pariatur.",
|
"ptate velit esse cillum dolore eu fugiat nulla <clipped message>",
|
||||||
"Excepteur sint occaecat cupidatat non proident <clipped message>",
|
" pariatur.",
|
||||||
", sunt in culpa qui officia deserunt mollit an <clipped message>",
|
"Excepteur sint occaecat cupidatat non proident <clipped message>",
|
||||||
"im id est laborum.",
|
", sunt in culpa qui officia deserunt mollit an <clipped message>",
|
||||||
},
|
"im id est laborum.",
|
||||||
nonSplitOutput: []string{
|
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
|
||||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
|
||||||
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
|
||||||
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"Message ending with new-line.": {
|
nonSplitOutput: []string{
|
||||||
input: "Newline ending\n",
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||||
splitOutput: []string{"Newline ending"},
|
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
||||||
nonSplitOutput: []string{"Newline ending"},
|
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
||||||
|
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||||
},
|
},
|
||||||
"Long message containing UTF-8 multi-byte runes": {
|
},
|
||||||
input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說",
|
"Message ending with new-line.": {
|
||||||
splitOutput: []string{
|
input: "Newline ending\n",
|
||||||
"不布人個我此而及單石業喜資富下 <clipped message>",
|
splitOutput: []string{"Newline ending"},
|
||||||
"我河下日沒一我臺空達的常景便物 <clipped message>",
|
nonSplitOutput: []string{"Newline ending"},
|
||||||
"沒為……子大我別名解成?生賣的 <clipped message>",
|
},
|
||||||
"全直黑,我自我結毛分洲了世當, <clipped message>",
|
"Long message containing UTF-8 multi-byte runes": {
|
||||||
"是政福那是東;斯說",
|
input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說",
|
||||||
},
|
splitOutput: []string{
|
||||||
nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
|
"不布人個我此而及單石業喜資富下 <clipped message>",
|
||||||
|
"我河下日沒一我臺空達的常景便物 <clipped message>",
|
||||||
|
"沒為……子大我別名解成?生賣的 <clipped message>",
|
||||||
|
"全直黑,我自我結毛分洲了世當, <clipped message>",
|
||||||
|
"是政福那是東;斯說",
|
||||||
},
|
},
|
||||||
}
|
nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
|
||||||
)
|
},
|
||||||
|
"Long message, clip three-byte rune after two bytes": {
|
||||||
|
input: "x 人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。",
|
||||||
|
splitOutput: []string{
|
||||||
|
"x 人人生而自由,在尊嚴和權利上 <clipped message>",
|
||||||
|
"一律平等。 他們都具有理性和良知 <clipped message>",
|
||||||
|
",應該以兄弟情誼的精神對待彼此。",
|
||||||
|
},
|
||||||
|
nonSplitOutput: []string{"x 人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetSubLines(t *testing.T) {
|
func TestGetSubLines(t *testing.T) {
|
||||||
for testname, testcase := range lineSplittingTestCases {
|
for testname, testcase := range lineSplittingTestCases {
|
||||||
splitLines := GetSubLines(testcase.input, testLineLength)
|
splitLines := GetSubLines(testcase.input, testLineLength, "")
|
||||||
assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname)
|
assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname)
|
||||||
for _, splitLine := range splitLines {
|
for _, splitLine := range splitLines {
|
||||||
byteLength := len([]byte(splitLine))
|
byteLength := len([]byte(splitLine))
|
||||||
assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength)
|
assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
nonSplitLines := GetSubLines(testcase.input, 0)
|
nonSplitLines := GetSubLines(testcase.input, 0, "")
|
||||||
assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname)
|
assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,17 +117,122 @@ func TestConvertWebPToPNG(t *testing.T) {
|
|||||||
if os.Getenv("LOCAL_TEST") == "" {
|
if os.Getenv("LOCAL_TEST") == "" {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
|
||||||
input, err := ioutil.ReadFile("test.webp")
|
input, err := ioutil.ReadFile("test.webp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &input
|
d := &input
|
||||||
err = ConvertWebPToPNG(d)
|
err = ConvertWebPToPNG(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
err = ioutil.WriteFile("test.png", *d, 0644)
|
|
||||||
|
err = ioutil.WriteFile("test.png", *d, 0o644) // nolint:gosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var clippingOrSplittingTestCases = map[string]struct {
|
||||||
|
inputText string
|
||||||
|
clipSplitLength int
|
||||||
|
clippingMessage string
|
||||||
|
splitMax int
|
||||||
|
expectedOutput []string
|
||||||
|
}{
|
||||||
|
"Short single-line message, split 3": {
|
||||||
|
inputText: "short",
|
||||||
|
clipSplitLength: 20,
|
||||||
|
clippingMessage: "?!?!",
|
||||||
|
splitMax: 3,
|
||||||
|
expectedOutput: []string{"short"},
|
||||||
|
},
|
||||||
|
"Short single-line message, split 1": {
|
||||||
|
inputText: "short",
|
||||||
|
clipSplitLength: 20,
|
||||||
|
clippingMessage: "?!?!",
|
||||||
|
splitMax: 1,
|
||||||
|
expectedOutput: []string{"short"},
|
||||||
|
},
|
||||||
|
"Short single-line message, split 0": {
|
||||||
|
// Mainly check that we don't crash.
|
||||||
|
inputText: "short",
|
||||||
|
clipSplitLength: 20,
|
||||||
|
clippingMessage: "?!?!",
|
||||||
|
splitMax: 0,
|
||||||
|
expectedOutput: []string{"short"},
|
||||||
|
},
|
||||||
|
"Long single-line message, noclip": {
|
||||||
|
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||||
|
clipSplitLength: 50,
|
||||||
|
clippingMessage: "?!?!",
|
||||||
|
splitMax: 10,
|
||||||
|
expectedOutput: []string{
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing",
|
||||||
|
" elit, sed do eiusmod tempor incididunt ut labore ",
|
||||||
|
"et dolore magna aliqua.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Long single-line message, noclip tight": {
|
||||||
|
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||||
|
clipSplitLength: 50,
|
||||||
|
clippingMessage: "?!?!",
|
||||||
|
splitMax: 3,
|
||||||
|
expectedOutput: []string{
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing",
|
||||||
|
" elit, sed do eiusmod tempor incididunt ut labore ",
|
||||||
|
"et dolore magna aliqua.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Long single-line message, clip custom": {
|
||||||
|
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||||
|
clipSplitLength: 50,
|
||||||
|
clippingMessage: "?!?!",
|
||||||
|
splitMax: 2,
|
||||||
|
expectedOutput: []string{
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing",
|
||||||
|
" elit, sed do eiusmod tempor incididunt ut lab?!?!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Long single-line message, clip built-in": {
|
||||||
|
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||||
|
clipSplitLength: 50,
|
||||||
|
clippingMessage: "",
|
||||||
|
splitMax: 2,
|
||||||
|
expectedOutput: []string{
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing",
|
||||||
|
" elit, sed do eiusmod tempor inc <clipped message>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Short multi-line message": {
|
||||||
|
inputText: "I\ncan't\nget\nno\nsatisfaction!",
|
||||||
|
clipSplitLength: 50,
|
||||||
|
clippingMessage: "",
|
||||||
|
splitMax: 2,
|
||||||
|
expectedOutput: []string{"I\ncan't\nget\nno\nsatisfaction!"},
|
||||||
|
},
|
||||||
|
"Long message containing UTF-8 multi-byte runes": {
|
||||||
|
inputText: "人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。",
|
||||||
|
clipSplitLength: 50,
|
||||||
|
clippingMessage: "",
|
||||||
|
splitMax: 10,
|
||||||
|
expectedOutput: []string{
|
||||||
|
"人人生而自由,在尊嚴和權利上一律", // Note: only 48 bytes!
|
||||||
|
"平等。 他們都具有理性和良知,應該", // Note: only 49 bytes!
|
||||||
|
"以兄弟情誼的精神對待彼此。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClipOrSplitMessage(t *testing.T) {
|
||||||
|
for testname, testcase := range clippingOrSplittingTestCases {
|
||||||
|
actualOutput := ClipOrSplitMessage(testcase.inputText, testcase.clipSplitLength, testcase.clippingMessage, testcase.splitMax)
|
||||||
|
assert.Equalf(t, testcase.expectedOutput, actualOutput, "'%s' testcase should give expected lines with clipping+splitting.", testname)
|
||||||
|
for _, splitLine := range testcase.expectedOutput {
|
||||||
|
byteLength := len([]byte(splitLine))
|
||||||
|
assert.True(t, byteLength <= testcase.clipSplitLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testname, testcase.clipSplitLength, byteLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
35
bridge/helper/libtgsconverter.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//go:build cgolottie
|
||||||
|
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Benau/tgsconverter/libtgsconverter"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CanConvertTgsToX() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertTgsToX convert input data (which should be tgs format) to any format supported by libtgsconverter
|
||||||
|
func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
|
||||||
|
options := libtgsconverter.NewConverterOptions()
|
||||||
|
options.SetExtension(outputFormat)
|
||||||
|
blob, err := libtgsconverter.ImportFromData(*data, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run libtgsconverter.ImportFromData: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
*data = blob
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SupportsFormat(format string) bool {
|
||||||
|
return libtgsconverter.SupportsExtension(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LottieBackend() string {
|
||||||
|
return "libtgsconverter"
|
||||||
|
}
|
90
bridge/helper/lottie_convert.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
//go:build !cgolottie
|
||||||
|
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CanConvertTgsToX Checks whether the external command necessary for ConvertTgsToX works.
|
||||||
|
func CanConvertTgsToX() error {
|
||||||
|
// We depend on the fact that `lottie_convert.py --help` has exit status 0.
|
||||||
|
// Hyrum's Law predicted this, and Murphy's Law predicts that this will break eventually.
|
||||||
|
// However, there is no alternative like `lottie_convert.py --is-properly-installed`
|
||||||
|
cmd := exec.Command("lottie_convert.py", "--help")
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertTgsToWebP convert input data (which should be tgs format) to WebP format
|
||||||
|
// This relies on an external command, which is ugly, but works.
|
||||||
|
func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
|
||||||
|
// lottie can't handle input from a pipe, so write to a temporary file:
|
||||||
|
tmpInFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-input-*.tgs")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmpInFileName := tmpInFile.Name()
|
||||||
|
defer func() {
|
||||||
|
if removeErr := os.Remove(tmpInFileName); removeErr != nil {
|
||||||
|
logger.Errorf("Could not delete temporary (input) file %s: %v", tmpInFileName, removeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// lottie can handle writing to a pipe, but there is no way to do that platform-independently.
|
||||||
|
// "/dev/stdout" won't work on Windows, and "-" upsets Cairo for some reason. So we need another file:
|
||||||
|
tmpOutFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-output-*.data")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmpOutFileName := tmpOutFile.Name()
|
||||||
|
defer func() {
|
||||||
|
if removeErr := os.Remove(tmpOutFileName); removeErr != nil {
|
||||||
|
logger.Errorf("Could not delete temporary (output) file %s: %v", tmpOutFileName, removeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, writeErr := tmpInFile.Write(*data); writeErr != nil {
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
// Must close before calling lottie to avoid data races:
|
||||||
|
if closeErr := tmpInFile.Close(); closeErr != nil {
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call lottie to transform:
|
||||||
|
cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpInFileName, tmpOutFileName)
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
// NB: lottie writes progress into to stderr in all cases.
|
||||||
|
_, stderr := cmd.Output()
|
||||||
|
if stderr != nil {
|
||||||
|
// 'stderr' already contains some parts of Stderr, because it was set to 'nil'.
|
||||||
|
return stderr
|
||||||
|
}
|
||||||
|
dataContents, err := ioutil.ReadFile(tmpOutFileName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*data = dataContents
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SupportsFormat(format string) bool {
|
||||||
|
switch format {
|
||||||
|
case "png":
|
||||||
|
fallthrough
|
||||||
|
case "webp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func LottieBackend() string {
|
||||||
|
return "lottie_convert.py"
|
||||||
|
}
|
32
bridge/irc/charset.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package birc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/text/encoding"
|
||||||
|
"golang.org/x/text/encoding/japanese"
|
||||||
|
"golang.org/x/text/encoding/korean"
|
||||||
|
"golang.org/x/text/encoding/simplifiedchinese"
|
||||||
|
"golang.org/x/text/encoding/traditionalchinese"
|
||||||
|
"golang.org/x/text/encoding/unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var encoders = map[string]encoding.Encoding{
|
||||||
|
"utf-8": unicode.UTF8,
|
||||||
|
"iso-2022-jp": japanese.ISO2022JP,
|
||||||
|
"big5": traditionalchinese.Big5,
|
||||||
|
"gbk": simplifiedchinese.GBK,
|
||||||
|
"euc-kr": korean.EUCKR,
|
||||||
|
"gb2312": simplifiedchinese.HZGB2312,
|
||||||
|
"shift-jis": japanese.ShiftJIS,
|
||||||
|
"euc-jp": japanese.EUCJP,
|
||||||
|
"gb18030": simplifiedchinese.GB18030,
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUTF8(from string, input string) string {
|
||||||
|
enc, ok := encoders[from]
|
||||||
|
if !ok {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
res, _ := enc.NewDecoder().String(input)
|
||||||
|
return res
|
||||||
|
}
|
@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
"github.com/lrstanley/girc"
|
"github.com/lrstanley/girc"
|
||||||
"github.com/missdeer/golib/ic"
|
|
||||||
"github.com/paulrosania/go-charset/charset"
|
"github.com/paulrosania/go-charset/charset"
|
||||||
"github.com/saintfish/chardet"
|
"github.com/saintfish/chardet"
|
||||||
|
|
||||||
@ -24,12 +23,12 @@ func (b *Birc) handleCharset(msg *config.Message) error {
|
|||||||
if b.GetString("Charset") != "" {
|
if b.GetString("Charset") != "" {
|
||||||
switch b.GetString("Charset") {
|
switch b.GetString("Charset") {
|
||||||
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
|
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
|
||||||
msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text)
|
msg.Text = toUTF8(b.GetString("Charset"), msg.Text)
|
||||||
default:
|
default:
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
w, err := charset.NewWriter(b.GetString("Charset"), buf)
|
w, err := charset.NewWriter(b.GetString("Charset"), buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("charset from utf-8 conversion failed: %s", err)
|
b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Fprint(w, msg.Text)
|
fmt.Fprint(w, msg.Text)
|
||||||
@ -67,6 +66,20 @@ func (b *Birc) handleFiles(msg *config.Message) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Birc) handleInvite(client *girc.Client, event girc.Event) {
|
||||||
|
if len(event.Params) != 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := event.Params[1]
|
||||||
|
|
||||||
|
b.Log.Debugf("got invite for %s", channel)
|
||||||
|
|
||||||
|
if _, ok := b.channels[channel]; ok {
|
||||||
|
b.i.Cmd.Join(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
|
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
|
||||||
if len(event.Params) == 0 {
|
if len(event.Params) == 0 {
|
||||||
b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
|
b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
|
||||||
@ -109,14 +122,25 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
|
|||||||
i := b.i
|
i := b.i
|
||||||
b.Nick = event.Params[0]
|
b.Nick = event.Params[0]
|
||||||
|
|
||||||
i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
|
b.Log.Debug("Clearing handlers before adding in case of BNC reconnect")
|
||||||
i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg)
|
i.Handlers.Clear("PRIVMSG")
|
||||||
|
i.Handlers.Clear("CTCP_ACTION")
|
||||||
|
i.Handlers.Clear(girc.RPL_TOPICWHOTIME)
|
||||||
|
i.Handlers.Clear(girc.NOTICE)
|
||||||
|
i.Handlers.Clear("JOIN")
|
||||||
|
i.Handlers.Clear("PART")
|
||||||
|
i.Handlers.Clear("QUIT")
|
||||||
|
i.Handlers.Clear("KICK")
|
||||||
|
i.Handlers.Clear("INVITE")
|
||||||
|
|
||||||
|
i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg)
|
||||||
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
|
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
|
||||||
i.Handlers.Add(girc.NOTICE, b.handleNotice)
|
i.Handlers.AddBg(girc.NOTICE, b.handleNotice)
|
||||||
i.Handlers.Add("JOIN", b.handleJoinPart)
|
i.Handlers.AddBg("JOIN", b.handleJoinPart)
|
||||||
i.Handlers.Add("PART", b.handleJoinPart)
|
i.Handlers.AddBg("PART", b.handleJoinPart)
|
||||||
i.Handlers.Add("QUIT", b.handleJoinPart)
|
i.Handlers.AddBg("QUIT", b.handleJoinPart)
|
||||||
i.Handlers.Add("KICK", b.handleJoinPart)
|
i.Handlers.AddBg("KICK", b.handleJoinPart)
|
||||||
|
i.Handlers.Add("INVITE", b.handleInvite)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) handleNickServ() {
|
func (b *Birc) handleNickServ() {
|
||||||
@ -181,7 +205,11 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
|
|||||||
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
|
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
|
||||||
|
|
||||||
// set action event
|
// set action event
|
||||||
if event.IsAction() {
|
if ok, ctcp := event.IsCTCP(); ok {
|
||||||
|
if ctcp.Command != girc.CTCP_ACTION {
|
||||||
|
b.Log.Debugf("dropping user ctcp, command: %s", ctcp.Command)
|
||||||
|
return
|
||||||
|
}
|
||||||
rmsg.Event = config.EventUserAction
|
rmsg.Event = config.EventUserAction
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +240,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
|
|||||||
}
|
}
|
||||||
switch mycharset {
|
switch mycharset {
|
||||||
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
|
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
|
||||||
rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text)
|
rmsg.Text = toUTF8(b.GetString("Charset"), rmsg.Text)
|
||||||
default:
|
default:
|
||||||
r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
|
r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -229,6 +257,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
|
|||||||
|
|
||||||
func (b *Birc) handleRunCommands() {
|
func (b *Birc) handleRunCommands() {
|
||||||
for _, cmd := range b.GetStringSlice("RunCommands") {
|
for _, cmd := range b.GetStringSlice("RunCommands") {
|
||||||
|
cmd = strings.ReplaceAll(cmd, "{BOTNICK}", b.Nick)
|
||||||
if err := b.i.Cmd.SendRaw(cmd); err != nil {
|
if err := b.i.Cmd.SendRaw(cmd); err != nil {
|
||||||
b.Log.Errorf("RunCommands %s failed: %s", cmd, err)
|
b.Log.Errorf("RunCommands %s failed: %s", cmd, err)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package birc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -30,6 +31,7 @@ type Birc struct {
|
|||||||
Local chan config.Message // local queue for flood control
|
Local chan config.Message // local queue for flood control
|
||||||
FirstConnection, authDone bool
|
FirstConnection, authDone bool
|
||||||
MessageDelay, MessageQueue, MessageLength int
|
MessageDelay, MessageQueue, MessageLength int
|
||||||
|
channels map[string]bool
|
||||||
|
|
||||||
*bridge.Config
|
*bridge.Config
|
||||||
}
|
}
|
||||||
@ -40,6 +42,8 @@ func New(cfg *bridge.Config) bridge.Bridger {
|
|||||||
b.Nick = b.GetString("Nick")
|
b.Nick = b.GetString("Nick")
|
||||||
b.names = make(map[string][]string)
|
b.names = make(map[string][]string)
|
||||||
b.connected = make(chan error)
|
b.connected = make(chan error)
|
||||||
|
b.channels = make(map[string]bool)
|
||||||
|
|
||||||
if b.GetInt("MessageDelay") == 0 {
|
if b.GetInt("MessageDelay") == 0 {
|
||||||
b.MessageDelay = 1300
|
b.MessageDelay = 1300
|
||||||
} else {
|
} else {
|
||||||
@ -69,6 +73,10 @@ func (b *Birc) Command(msg *config.Message) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) Connect() error {
|
func (b *Birc) Connect() error {
|
||||||
|
if b.GetBool("UseSASL") && b.GetString("TLSClientCertificate") != "" {
|
||||||
|
return errors.New("you can't enable SASL and TLSClientCertificate at the same time")
|
||||||
|
}
|
||||||
|
|
||||||
b.Local = make(chan config.Message, b.MessageQueue+10)
|
b.Local = make(chan config.Message, b.MessageQueue+10)
|
||||||
b.Log.Infof("Connecting %s", b.GetString("Server"))
|
b.Log.Infof("Connecting %s", b.GetString("Server"))
|
||||||
|
|
||||||
@ -112,6 +120,7 @@ func (b *Birc) Disconnect() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
|
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
b.channels[channel.Name] = true
|
||||||
// need to check if we have nickserv auth done before joining channels
|
// need to check if we have nickserv auth done before joining channels
|
||||||
for {
|
for {
|
||||||
if b.authDone {
|
if b.authDone {
|
||||||
@ -163,9 +172,9 @@ func (b *Birc) Send(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b.GetBool("MessageSplit") {
|
if b.GetBool("MessageSplit") {
|
||||||
msgLines = helper.GetSubLines(msg.Text, b.MessageLength)
|
msgLines = helper.GetSubLines(msg.Text, b.MessageLength, b.GetString("MessageClipped"))
|
||||||
} else {
|
} else {
|
||||||
msgLines = helper.GetSubLines(msg.Text, 0)
|
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
|
||||||
}
|
}
|
||||||
for i := range msgLines {
|
for i := range msgLines {
|
||||||
if len(b.Local) >= b.MessageQueue {
|
if len(b.Local) >= b.MessageQueue {
|
||||||
@ -201,27 +210,58 @@ func (b *Birc) doConnect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitize nicks for RELAYMSG: replace IRC characters with special meanings with "-"
|
||||||
|
func sanitizeNick(nick string) string {
|
||||||
|
sanitize := func(r rune) rune {
|
||||||
|
if strings.ContainsRune("!+%@&#$:'\"?*,. ", r) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return strings.Map(sanitize, nick)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Birc) doSend() {
|
func (b *Birc) doSend() {
|
||||||
rate := time.Millisecond * time.Duration(b.MessageDelay)
|
rate := time.Millisecond * time.Duration(b.MessageDelay)
|
||||||
throttle := time.NewTicker(rate)
|
throttle := time.NewTicker(rate)
|
||||||
for msg := range b.Local {
|
for msg := range b.Local {
|
||||||
<-throttle.C
|
<-throttle.C
|
||||||
username := msg.Username
|
username := msg.Username
|
||||||
if b.GetBool("Colornicks") && len(username) > 1 {
|
// Optional support for the proposed RELAYMSG extension, described at
|
||||||
checksum := crc32.ChecksumIEEE([]byte(msg.Username))
|
// https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md
|
||||||
colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
|
// nolint:nestif
|
||||||
username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
|
if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) &&
|
||||||
}
|
b.GetBool("UseRelayMsg") {
|
||||||
|
username = sanitizeNick(username)
|
||||||
|
text := msg.Text
|
||||||
|
|
||||||
switch msg.Event {
|
// Work around girc chomping leading commas on single word messages?
|
||||||
case config.EventUserAction:
|
if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') {
|
||||||
b.i.Cmd.Action(msg.Channel, username+msg.Text)
|
text = ":" + text
|
||||||
case config.EventNoticeIRC:
|
}
|
||||||
b.Log.Debugf("Sending notice to channel %s", msg.Channel)
|
|
||||||
b.i.Cmd.Notice(msg.Channel, username+msg.Text)
|
if msg.Event == config.EventUserAction {
|
||||||
default:
|
b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck
|
||||||
b.Log.Debugf("Sending to channel %s", msg.Channel)
|
} else {
|
||||||
b.i.Cmd.Message(msg.Channel, username+msg.Text)
|
b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username)
|
||||||
|
b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -236,8 +276,11 @@ func (b *Birc) getClient() (*girc.Client, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
user := b.GetString("UserName")
|
||||||
|
if user == "" {
|
||||||
|
user = b.GetString("Nick")
|
||||||
|
}
|
||||||
// fix strict user handling of girc
|
// fix strict user handling of girc
|
||||||
user := b.GetString("Nick")
|
|
||||||
for !girc.IsValidUser(user) {
|
for !girc.IsValidUser(user) {
|
||||||
if len(user) == 1 || len(user) == 0 {
|
if len(user) == 1 || len(user) == 0 {
|
||||||
user = "matterbridge"
|
user = "matterbridge"
|
||||||
@ -245,6 +288,10 @@ func (b *Birc) getClient() (*girc.Client, error) {
|
|||||||
}
|
}
|
||||||
user = user[1:]
|
user = user[1:]
|
||||||
}
|
}
|
||||||
|
realName := b.GetString("RealName")
|
||||||
|
if realName == "" {
|
||||||
|
realName = b.GetString("Nick")
|
||||||
|
}
|
||||||
|
|
||||||
debug := ioutil.Discard
|
debug := ioutil.Discard
|
||||||
if b.GetInt("DebugLevel") == 2 {
|
if b.GetInt("DebugLevel") == 2 {
|
||||||
@ -258,19 +305,26 @@ func (b *Birc) getClient() (*girc.Client, error) {
|
|||||||
|
|
||||||
b.Log.Debugf("setting pingdelay to %s", pingDelay)
|
b.Log.Debugf("setting pingdelay to %s", pingDelay)
|
||||||
|
|
||||||
|
tlsConfig, err := b.getTLSConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
i := girc.New(girc.Config{
|
i := girc.New(girc.Config{
|
||||||
Server: server,
|
Server: server,
|
||||||
ServerPass: b.GetString("Password"),
|
ServerPass: b.GetString("Password"),
|
||||||
Port: port,
|
Port: port,
|
||||||
Nick: b.GetString("Nick"),
|
Nick: b.GetString("Nick"),
|
||||||
User: user,
|
User: user,
|
||||||
Name: b.GetString("Nick"),
|
Name: realName,
|
||||||
SSL: b.GetBool("UseTLS"),
|
SSL: b.GetBool("UseTLS"),
|
||||||
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec
|
Bind: b.GetString("Bind"),
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
PingDelay: pingDelay,
|
PingDelay: pingDelay,
|
||||||
// skip gIRC internal rate limiting, since we have our own throttling
|
// skip gIRC internal rate limiting, since we have our own throttling
|
||||||
AllowFlood: true,
|
AllowFlood: true,
|
||||||
Debug: debug,
|
Debug: debug,
|
||||||
|
SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil},
|
||||||
})
|
})
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
@ -280,12 +334,16 @@ func (b *Birc) endNames(client *girc.Client, event girc.Event) {
|
|||||||
sort.Strings(b.names[channel])
|
sort.Strings(b.names[channel])
|
||||||
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
|
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
|
||||||
for len(b.names[channel]) > maxNamesPerPost {
|
for len(b.names[channel]) > maxNamesPerPost {
|
||||||
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]),
|
b.Remote <- config.Message{
|
||||||
Channel: channel, Account: b.Account}
|
Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]),
|
||||||
|
Channel: channel, Account: b.Account,
|
||||||
|
}
|
||||||
b.names[channel] = b.names[channel][maxNamesPerPost:]
|
b.names[channel] = b.names[channel][maxNamesPerPost:]
|
||||||
}
|
}
|
||||||
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel]),
|
b.Remote <- config.Message{
|
||||||
Channel: channel, Account: b.Account}
|
Username: b.Nick, Text: b.formatnicks(b.names[channel]),
|
||||||
|
Channel: channel, Account: b.Account,
|
||||||
|
}
|
||||||
b.names[channel] = nil
|
b.names[channel] = nil
|
||||||
b.i.Handlers.Clear(girc.RPL_NAMREPLY)
|
b.i.Handlers.Clear(girc.RPL_NAMREPLY)
|
||||||
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
|
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
|
||||||
@ -304,7 +362,18 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// don't forward message from ourself
|
// don't forward message from ourself
|
||||||
if event.Source.Name == b.Nick {
|
if event.Source != nil {
|
||||||
|
if event.Source.Name == b.Nick {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// don't forward messages we sent via RELAYMSG
|
||||||
|
if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// This is the old name of the cap sent in spoofed messages; I've kept this in
|
||||||
|
// for compatibility reasons
|
||||||
|
if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -324,3 +393,23 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
|
|||||||
func (b *Birc) formatnicks(nicks []string) string {
|
func (b *Birc) formatnicks(nicks []string) string {
|
||||||
return strings.Join(nicks, ", ") + " currently on IRC"
|
return strings.Join(nicks, ", ") + " currently on IRC"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Birc) getTLSConfig() (*tls.Config, error) {
|
||||||
|
server, _, _ := net.SplitHostPort(b.GetString("server"))
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: b.GetBool("skiptlsverify"), //nolint:gosec
|
||||||
|
ServerName: server,
|
||||||
|
}
|
||||||
|
|
||||||
|
if filename := b.GetString("TLSClientCertificate"); filename != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(filename, filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsConfig, nil
|
||||||
|
}
|
||||||
|
@ -3,11 +3,12 @@ package bmatrix
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
matrix "github.com/matrix-org/gomatrix"
|
matrix "github.com/matterbridge/gomatrix"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newMatrixUsername(username string) *matrixUsername {
|
func newMatrixUsername(username string) *matrixUsername {
|
||||||
@ -50,7 +51,7 @@ func interface2Struct(in interface{}, out interface{}) error {
|
|||||||
return json.Unmarshal(jsonObj, out)
|
return json.Unmarshal(jsonObj, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDisplayName retrieves the displayName for mxid, querying the homserver if the mxid is not in the cache.
|
// getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache.
|
||||||
func (b *Bmatrix) getDisplayName(mxid string) string {
|
func (b *Bmatrix) getDisplayName(mxid string) string {
|
||||||
if b.GetBool("UseUserName") {
|
if b.GetBool("UseUserName") {
|
||||||
return mxid[1:]
|
return mxid[1:]
|
||||||
@ -82,20 +83,36 @@ func (b *Bmatrix) getDisplayName(mxid string) string {
|
|||||||
func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
|
func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
// scan to delete old entries, to stop memory usage from becoming too high with old entries
|
// 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{}
|
toDelete := []string{}
|
||||||
b.RLock()
|
conflict := false
|
||||||
for k, v := range b.NicknameMap {
|
|
||||||
if now.Sub(v.lastUpdated) > 10*time.Minute {
|
|
||||||
toDelete = append(toDelete, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.RUnlock()
|
|
||||||
|
|
||||||
b.Lock()
|
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 {
|
for _, v := range toDelete {
|
||||||
delete(b.NicknameMap, v)
|
delete(b.NicknameMap, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.NicknameMap[mxid] = NicknameCacheEntry{
|
b.NicknameMap[mxid] = NicknameCacheEntry{
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
lastUpdated: now,
|
lastUpdated: now,
|
||||||
@ -164,3 +181,35 @@ func (b *Bmatrix) getAvatarURL(sender string) string {
|
|||||||
|
|
||||||
return url
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/42wim/matterbridge/bridge"
|
"github.com/42wim/matterbridge/bridge"
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
matrix "github.com/matrix-org/gomatrix"
|
matrix "github.com/matterbridge/gomatrix"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -30,6 +30,7 @@ type Bmatrix struct {
|
|||||||
UserID string
|
UserID string
|
||||||
NicknameMap map[string]NicknameCacheEntry
|
NicknameMap map[string]NicknameCacheEntry
|
||||||
RoomMap map[string]string
|
RoomMap map[string]string
|
||||||
|
rateMutex sync.RWMutex
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
*bridge.Config
|
*bridge.Config
|
||||||
}
|
}
|
||||||
@ -47,8 +48,10 @@ type matrixUsername struct {
|
|||||||
|
|
||||||
// SubTextMessage represents the new content of the message in edit messages.
|
// SubTextMessage represents the new content of the message in edit messages.
|
||||||
type SubTextMessage struct {
|
type SubTextMessage struct {
|
||||||
MsgType string `json:"msgtype"`
|
MsgType string `json:"msgtype"`
|
||||||
Body string `json:"body"`
|
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.
|
// MessageRelation explains how the current message relates to a previous message.
|
||||||
@ -64,6 +67,19 @@ type EditedMessage struct {
|
|||||||
matrix.TextMessage
|
matrix.TextMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InReplyToRelationContent struct {
|
||||||
|
EventID string `json:"event_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InReplyToRelation struct {
|
||||||
|
InReplyTo InReplyToRelationContent `json:"m.in_reply_to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplyMessage struct {
|
||||||
|
RelatedTo InReplyToRelation `json:"m.relates_to"`
|
||||||
|
matrix.TextMessage
|
||||||
|
}
|
||||||
|
|
||||||
func New(cfg *bridge.Config) bridge.Bridger {
|
func New(cfg *bridge.Config) bridge.Bridger {
|
||||||
b := &Bmatrix{Config: cfg}
|
b := &Bmatrix{Config: cfg}
|
||||||
b.RoomMap = make(map[string]string)
|
b.RoomMap = make(map[string]string)
|
||||||
@ -74,22 +90,33 @@ func New(cfg *bridge.Config) bridge.Bridger {
|
|||||||
func (b *Bmatrix) Connect() error {
|
func (b *Bmatrix) Connect() error {
|
||||||
var err error
|
var err error
|
||||||
b.Log.Infof("Connecting %s", b.GetString("Server"))
|
b.Log.Infof("Connecting %s", b.GetString("Server"))
|
||||||
b.mc, err = matrix.NewClient(b.GetString("Server"), "", "")
|
if b.GetString("MxID") != "" && b.GetString("Token") != "" {
|
||||||
if err != nil {
|
b.mc, err = matrix.NewClient(
|
||||||
return err
|
b.GetString("Server"), b.GetString("MxID"), b.GetString("Token"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.UserID = b.GetString("MxID")
|
||||||
|
b.Log.Info("Using existing Matrix credentials")
|
||||||
|
} else {
|
||||||
|
b.mc, err = matrix.NewClient(b.GetString("Server"), "", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := b.mc.Login(&matrix.ReqLogin{
|
||||||
|
Type: "m.login.password",
|
||||||
|
User: b.GetString("Login"),
|
||||||
|
Password: b.GetString("Password"),
|
||||||
|
Identifier: matrix.NewUserIdentifier(b.GetString("Login")),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.mc.SetCredentials(resp.UserID, resp.AccessToken)
|
||||||
|
b.UserID = resp.UserID
|
||||||
|
b.Log.Info("Connection succeeded")
|
||||||
}
|
}
|
||||||
resp, err := b.mc.Login(&matrix.ReqLogin{
|
|
||||||
Type: "m.login.password",
|
|
||||||
User: b.GetString("Login"),
|
|
||||||
Password: b.GetString("Password"),
|
|
||||||
Identifier: matrix.NewUserIdentifier(b.GetString("Login")),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.mc.SetCredentials(resp.UserID, resp.AccessToken)
|
|
||||||
b.UserID = resp.UserID
|
|
||||||
b.Log.Info("Connection succeeded")
|
|
||||||
go b.handlematrix()
|
go b.handlematrix()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -99,25 +126,18 @@ func (b *Bmatrix) Disconnect() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
|
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
|
||||||
retry:
|
return b.retry(func() error {
|
||||||
resp, err := b.mc.JoinRoom(channel.Name, "", nil)
|
resp, err := b.mc.JoinRoom(channel.Name, "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpErr := handleError(err)
|
return err
|
||||||
if httpErr.Errcode == "M_LIMIT_EXCEEDED" {
|
|
||||||
b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before joining %s", httpErr.RetryAfterMs/1000, channel.Name)
|
|
||||||
time.Sleep((time.Duration(httpErr.RetryAfterMs) * time.Millisecond))
|
|
||||||
|
|
||||||
goto retry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
b.Lock()
|
||||||
}
|
b.RoomMap[resp.RoomID] = channel.Name
|
||||||
|
b.Unlock()
|
||||||
|
|
||||||
b.Lock()
|
return nil
|
||||||
b.RoomMap[resp.RoomID] = channel.Name
|
})
|
||||||
b.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bmatrix) Send(msg config.Message) (string, error) {
|
func (b *Bmatrix) Send(msg config.Message) (string, error) {
|
||||||
@ -128,18 +148,59 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
|
|||||||
|
|
||||||
username := newMatrixUsername(msg.Username)
|
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
|
// Make a action /me of the message
|
||||||
if msg.Event == config.EventUserAction {
|
if msg.Event == config.EventUserAction {
|
||||||
m := matrix.TextMessage{
|
m := matrix.TextMessage{
|
||||||
MsgType: "m.emote",
|
MsgType: "m.emote",
|
||||||
Body: username.plain + msg.Text,
|
Body: body,
|
||||||
FormattedBody: username.formatted + msg.Text,
|
FormattedBody: formattedBody,
|
||||||
|
Format: "org.matrix.custom.html",
|
||||||
}
|
}
|
||||||
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
|
|
||||||
if err != nil {
|
if b.GetBool("HTMLDisable") {
|
||||||
return "", err
|
m.Format = ""
|
||||||
|
m.FormattedBody = ""
|
||||||
}
|
}
|
||||||
return resp.EventID, err
|
|
||||||
|
msgID := ""
|
||||||
|
|
||||||
|
err := b.retry(func() error {
|
||||||
|
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msgID = resp.EventID
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return msgID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete message
|
// Delete message
|
||||||
@ -147,17 +208,34 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
|
|||||||
if msg.ID == "" {
|
if msg.ID == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
|
|
||||||
if err != nil {
|
msgID := ""
|
||||||
return "", err
|
|
||||||
}
|
err := b.retry(func() error {
|
||||||
return resp.EventID, err
|
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msgID = resp.EventID
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return msgID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload a file if it exists
|
// Upload a file if it exists
|
||||||
if msg.Extra != nil {
|
if msg.Extra != nil {
|
||||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||||
if _, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text); err != nil {
|
rmsg := rmsg
|
||||||
|
|
||||||
|
err := b.retry(func() error {
|
||||||
|
_, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
b.Log.Errorf("sendText failed: %s", err)
|
b.Log.Errorf("sendText failed: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,25 +247,39 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
|
|||||||
|
|
||||||
// Edit message if we have an ID
|
// Edit message if we have an ID
|
||||||
if msg.ID != "" {
|
if msg.ID != "" {
|
||||||
rmsg := EditedMessage{TextMessage: matrix.TextMessage{
|
rmsg := EditedMessage{
|
||||||
Body: username.plain + msg.Text,
|
TextMessage: matrix.TextMessage{
|
||||||
MsgType: "m.text",
|
Body: body,
|
||||||
}}
|
MsgType: "m.text",
|
||||||
if b.GetBool("HTMLDisable") {
|
Format: "org.matrix.custom.html",
|
||||||
rmsg.TextMessage.FormattedBody = username.formatted + "* " + msg.Text
|
FormattedBody: formattedBody,
|
||||||
} else {
|
},
|
||||||
rmsg.Format = "org.matrix.custom.html"
|
|
||||||
rmsg.TextMessage.FormattedBody = username.formatted + "* " + helper.ParseMarkdown(msg.Text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rmsg.NewContent = SubTextMessage{
|
rmsg.NewContent = SubTextMessage{
|
||||||
Body: rmsg.TextMessage.Body,
|
Body: rmsg.TextMessage.Body,
|
||||||
MsgType: "m.text",
|
FormattedBody: rmsg.TextMessage.FormattedBody,
|
||||||
|
Format: rmsg.TextMessage.Format,
|
||||||
|
MsgType: "m.text",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.GetBool("HTMLDisable") {
|
||||||
|
rmsg.TextMessage.Format = ""
|
||||||
|
rmsg.TextMessage.FormattedBody = ""
|
||||||
|
rmsg.NewContent.Format = ""
|
||||||
|
rmsg.NewContent.FormattedBody = ""
|
||||||
|
}
|
||||||
|
|
||||||
rmsg.RelatedTo = MessageRelation{
|
rmsg.RelatedTo = MessageRelation{
|
||||||
EventID: msg.ID,
|
EventID: msg.ID,
|
||||||
Type: "m.replace",
|
Type: "m.replace",
|
||||||
}
|
}
|
||||||
_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
|
|
||||||
|
err := b.retry(func() error {
|
||||||
|
_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -199,29 +291,104 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
|
|||||||
if msg.Event == config.EventJoinLeave {
|
if msg.Event == config.EventJoinLeave {
|
||||||
m := matrix.TextMessage{
|
m := matrix.TextMessage{
|
||||||
MsgType: "m.notice",
|
MsgType: "m.notice",
|
||||||
Body: username.plain + msg.Text,
|
Body: body,
|
||||||
FormattedBody: username.formatted + msg.Text,
|
FormattedBody: formattedBody,
|
||||||
|
Format: "org.matrix.custom.html",
|
||||||
}
|
}
|
||||||
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
|
|
||||||
|
if b.GetBool("HTMLDisable") {
|
||||||
|
m.Format = ""
|
||||||
|
m.FormattedBody = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
resp *matrix.RespSendEvent
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
err = b.retry(func() error {
|
||||||
|
resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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
|
return resp.EventID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.GetBool("HTMLDisable") {
|
if b.GetBool("HTMLDisable") {
|
||||||
resp, err := b.mc.SendText(channel, username.plain+msg.Text)
|
var (
|
||||||
|
resp *matrix.RespSendEvent
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
err = b.retry(func() error {
|
||||||
|
resp, err = b.mc.SendText(channel, body)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp.EventID, err
|
return resp.EventID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post normal message with HTML support (eg riot.im)
|
// Post normal message with HTML support (eg riot.im)
|
||||||
resp, err := b.mc.SendFormattedText(channel, username.plain+msg.Text, username.formatted+helper.ParseMarkdown(msg.Text))
|
var (
|
||||||
|
resp *matrix.RespSendEvent
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
err = b.retry(func() error {
|
||||||
|
resp, err = b.mc.SendFormattedText(channel, body, formattedBody)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp.EventID, err
|
return resp.EventID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,6 +399,9 @@ func (b *Bmatrix) handlematrix() {
|
|||||||
syncer.OnEventType("m.room.member", b.handleMemberChange)
|
syncer.OnEventType("m.room.member", b.handleMemberChange)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
|
if b == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := b.mc.Sync(); err != nil {
|
if err := b.mc.Sync(); err != nil {
|
||||||
b.Log.Println("Sync() returned ", err)
|
b.Log.Println("Sync() returned ", err)
|
||||||
}
|
}
|
||||||
@ -269,6 +439,38 @@ func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool {
|
|||||||
return true
|
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) {
|
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
|
// 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 ev.Content["membership"] == "join" {
|
||||||
@ -299,13 +501,6 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
|
|||||||
Avatar: b.getAvatarURL(ev.Sender),
|
Avatar: b.getAvatarURL(ev.Sender),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text must be a string
|
|
||||||
if rmsg.Text, ok = ev.Content["body"].(string); !ok {
|
|
||||||
b.Log.Errorf("Content[body] is not a string: %T\n%#v",
|
|
||||||
ev.Content["body"], ev.Content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove homeserver suffix if configured
|
// Remove homeserver suffix if configured
|
||||||
if b.GetBool("NoHomeServerSuffix") {
|
if b.GetBool("NoHomeServerSuffix") {
|
||||||
re := regexp.MustCompile("(.*?):.*")
|
re := regexp.MustCompile("(.*?):.*")
|
||||||
@ -321,6 +516,13 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Text must be a string
|
||||||
|
if rmsg.Text, ok = ev.Content["body"].(string); !ok {
|
||||||
|
b.Log.Errorf("Content[body] is not a string: %T\n%#v",
|
||||||
|
ev.Content["body"], ev.Content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Do we have a /me action
|
// Do we have a /me action
|
||||||
if ev.Content["msgtype"].(string) == "m.emote" {
|
if ev.Content["msgtype"].(string) == "m.emote" {
|
||||||
rmsg.Event = config.EventUserAction
|
rmsg.Event = config.EventUserAction
|
||||||
@ -331,6 +533,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is it a reply?
|
||||||
|
if b.handleReply(ev, rmsg) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Do we have attachments
|
// Do we have attachments
|
||||||
if b.containsAttachment(ev.Content) {
|
if b.containsAttachment(ev.Content) {
|
||||||
err := b.handleDownloadFile(&rmsg, ev.Content)
|
err := b.handleDownloadFile(&rmsg, ev.Content)
|
||||||
@ -341,6 +548,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
|
|||||||
|
|
||||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
|
||||||
b.Remote <- rmsg
|
b.Remote <- rmsg
|
||||||
|
|
||||||
|
// not crucial, so no ratelimit check here
|
||||||
|
if err := b.mc.MarkRead(ev.RoomID, ev.ID); err != nil {
|
||||||
|
b.Log.Errorf("couldn't mark message as read %s", err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -420,13 +632,25 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
|
|||||||
sp := strings.Split(fi.Name, ".")
|
sp := strings.Split(fi.Name, ".")
|
||||||
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
|
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
|
||||||
// image and video uploads send no username, we have to do this ourself here #715
|
// image and video uploads send no username, we have to do this ourself here #715
|
||||||
_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment)
|
err := b.retry(func() error {
|
||||||
|
_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("file comment failed: %#v", err)
|
b.Log.Errorf("file comment failed: %#v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
|
b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
|
||||||
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
|
|
||||||
|
var res *matrix.RespMediaUpload
|
||||||
|
|
||||||
|
err = b.retry(func() error {
|
||||||
|
res, err = b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("file upload failed: %#v", err)
|
b.Log.Errorf("file upload failed: %#v", err)
|
||||||
return
|
return
|
||||||
@ -435,40 +659,56 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
|
|||||||
switch {
|
switch {
|
||||||
case strings.Contains(mtype, "video"):
|
case strings.Contains(mtype, "video"):
|
||||||
b.Log.Debugf("sendVideo %s", res.ContentURI)
|
b.Log.Debugf("sendVideo %s", res.ContentURI)
|
||||||
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
|
err = b.retry(func() error {
|
||||||
|
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("sendVideo failed: %#v", err)
|
b.Log.Errorf("sendVideo failed: %#v", err)
|
||||||
}
|
}
|
||||||
case strings.Contains(mtype, "image"):
|
case strings.Contains(mtype, "image"):
|
||||||
b.Log.Debugf("sendImage %s", res.ContentURI)
|
b.Log.Debugf("sendImage %s", res.ContentURI)
|
||||||
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
|
err = b.retry(func() error {
|
||||||
|
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("sendImage failed: %#v", err)
|
b.Log.Errorf("sendImage failed: %#v", err)
|
||||||
}
|
}
|
||||||
case strings.Contains(mtype, "audio"):
|
case strings.Contains(mtype, "audio"):
|
||||||
b.Log.Debugf("sendAudio %s", res.ContentURI)
|
b.Log.Debugf("sendAudio %s", res.ContentURI)
|
||||||
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{
|
err = b.retry(func() error {
|
||||||
MsgType: "m.audio",
|
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{
|
||||||
Body: fi.Name,
|
MsgType: "m.audio",
|
||||||
URL: res.ContentURI,
|
Body: fi.Name,
|
||||||
Info: matrix.AudioInfo{
|
URL: res.ContentURI,
|
||||||
Mimetype: mtype,
|
Info: matrix.AudioInfo{
|
||||||
Size: uint(len(*fi.Data)),
|
Mimetype: mtype,
|
||||||
},
|
Size: uint(len(*fi.Data)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("sendAudio failed: %#v", err)
|
b.Log.Errorf("sendAudio failed: %#v", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
b.Log.Debugf("sendFile %s", res.ContentURI)
|
b.Log.Debugf("sendFile %s", res.ContentURI)
|
||||||
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{
|
err = b.retry(func() error {
|
||||||
MsgType: "m.file",
|
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{
|
||||||
Body: fi.Name,
|
MsgType: "m.file",
|
||||||
URL: res.ContentURI,
|
Body: fi.Name,
|
||||||
Info: matrix.FileInfo{
|
URL: res.ContentURI,
|
||||||
Mimetype: mtype,
|
Info: matrix.FileInfo{
|
||||||
Size: uint(len(*fi.Data)),
|
Mimetype: mtype,
|
||||||
},
|
Size: uint(len(*fi.Data)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("sendFile failed: %#v", err)
|
b.Log.Errorf("sendFile failed: %#v", err)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package bmattermost
|
package bmattermost
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
"github.com/42wim/matterbridge/matterclient"
|
"github.com/matterbridge/matterclient"
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleDownloadAvatar downloads the avatar of userid from channel
|
// handleDownloadAvatar downloads the avatar of userid from channel
|
||||||
@ -21,12 +23,17 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
|
|||||||
Extra: make(map[string][]interface{}),
|
Extra: make(map[string][]interface{}),
|
||||||
}
|
}
|
||||||
if _, ok := b.avatarMap[userid]; !ok {
|
if _, ok := b.avatarMap[userid]; !ok {
|
||||||
data, resp := b.mc.Client.GetProfileImage(userid, "")
|
var (
|
||||||
if resp.Error != nil {
|
data []byte
|
||||||
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
|
err error
|
||||||
|
)
|
||||||
|
data, _, err = b.mc.Client.GetProfileImage(context.TODO(), userid, "")
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
|
|
||||||
|
err = helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Error(err)
|
b.Log.Error(err)
|
||||||
return
|
return
|
||||||
@ -36,20 +43,20 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleDownloadFile handles file download
|
//nolint:wrapcheck
|
||||||
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
|
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
|
||||||
url, _ := b.mc.Client.GetFileLink(id)
|
url, _, _ := b.mc.Client.GetFileLink(context.TODO(), id)
|
||||||
finfo, resp := b.mc.Client.GetFileInfo(id)
|
finfo, _, err := b.mc.Client.GetFileInfo(context.TODO(), id)
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data, resp := b.mc.Client.DownloadFile(id, true)
|
err = helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
|
||||||
if resp.Error != nil {
|
if err != nil {
|
||||||
return resp.Error
|
return err
|
||||||
|
}
|
||||||
|
data, _, err := b.mc.Client.DownloadFile(context.TODO(), id, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
|
helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
|
||||||
return nil
|
return nil
|
||||||
@ -86,18 +93,24 @@ func (b *Bmattermost) handleMatter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:cyclop
|
||||||
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
|
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
|
||||||
for message := range b.mc.MessageChan {
|
for message := range b.mc.MessageChan {
|
||||||
b.Log.Debugf("%#v", message.Raw.Data)
|
b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType())
|
||||||
|
|
||||||
if b.skipMessage(message) {
|
if b.skipMessage(message) {
|
||||||
b.Log.Debugf("Skipped message: %#v", message)
|
b.Log.Debugf("Skipped message: %#v", message)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channelName := b.getChannelName(message.Post.ChannelId)
|
||||||
|
if channelName == "" {
|
||||||
|
channelName = message.Channel
|
||||||
|
}
|
||||||
|
|
||||||
// only download avatars if we have a place to upload them (configured mediaserver)
|
// only download avatars if we have a place to upload them (configured mediaserver)
|
||||||
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
|
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
|
||||||
b.handleDownloadAvatar(message.UserID, message.Channel)
|
b.handleDownloadAvatar(message.UserID, channelName)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Log.Debugf("== Receiving event %#v", message)
|
b.Log.Debugf("== Receiving event %#v", message)
|
||||||
@ -105,10 +118,10 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
|
|||||||
rmsg := &config.Message{
|
rmsg := &config.Message{
|
||||||
Username: message.Username,
|
Username: message.Username,
|
||||||
UserID: message.UserID,
|
UserID: message.UserID,
|
||||||
Channel: message.Channel,
|
Channel: channelName,
|
||||||
Text: message.Text,
|
Text: message.Text,
|
||||||
ID: message.Post.Id,
|
ID: message.Post.Id,
|
||||||
ParentID: message.Post.ParentId,
|
ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
|
||||||
Extra: make(map[string][]interface{}),
|
Extra: make(map[string][]interface{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,11 +129,11 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
|
|||||||
b.handleProps(rmsg, message)
|
b.handleProps(rmsg, message)
|
||||||
|
|
||||||
// create a text for bridges that don't support native editing
|
// create a text for bridges that don't support native editing
|
||||||
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") {
|
if message.Raw.EventType() == model.WebsocketEventPostEdited && !b.GetBool("EditDisable") {
|
||||||
rmsg.Text = message.Text + b.GetString("EditSuffix")
|
rmsg.Text = message.Text + b.GetString("EditSuffix")
|
||||||
}
|
}
|
||||||
|
|
||||||
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED {
|
if message.Raw.EventType() == model.WebsocketEventPostDeleted {
|
||||||
rmsg.Event = config.EventMsgDelete
|
rmsg.Event = config.EventMsgDelete
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,8 +145,10 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use nickname instead of username if defined
|
// Use nickname instead of username if defined
|
||||||
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
|
if !b.GetBool("useusername") {
|
||||||
rmsg.Username = nick
|
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
|
||||||
|
rmsg.Username = nick
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messages <- rmsg
|
messages <- rmsg
|
||||||
@ -144,6 +159,7 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
|
|||||||
for {
|
for {
|
||||||
message := b.mh.Receive()
|
message := b.mh.Receive()
|
||||||
b.Log.Debugf("Receiving from matterhook %#v", message)
|
b.Log.Debugf("Receiving from matterhook %#v", message)
|
||||||
|
|
||||||
messages <- &config.Message{
|
messages <- &config.Message{
|
||||||
UserID: message.UserID,
|
UserID: message.UserID,
|
||||||
Username: message.UserName,
|
Username: message.UserName,
|
||||||
@ -153,11 +169,10 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleUploadFile handles native upload of files
|
|
||||||
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
|
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
|
||||||
var err error
|
var err error
|
||||||
var res, id string
|
var res, id string
|
||||||
channelID := b.mc.GetChannelId(msg.Channel, b.TeamID)
|
channelID := b.getChannelID(msg.Channel)
|
||||||
for _, f := range msg.Extra["file"] {
|
for _, f := range msg.Extra["file"] {
|
||||||
fi := f.(config.FileInfo)
|
fi := f.(config.FileInfo)
|
||||||
id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
|
id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
|
||||||
@ -173,6 +188,7 @@ func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:forcetypeassert
|
||||||
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
|
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
|
||||||
props := message.Post.Props
|
props := message.Post.Props
|
||||||
if props == nil {
|
if props == nil {
|
||||||
@ -183,16 +199,18 @@ func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Me
|
|||||||
}
|
}
|
||||||
if _, ok := props["attachments"].([]interface{}); ok {
|
if _, ok := props["attachments"].([]interface{}); ok {
|
||||||
rmsg.Extra["attachments"] = props["attachments"].([]interface{})
|
rmsg.Extra["attachments"] = props["attachments"].([]interface{})
|
||||||
if rmsg.Text == "" {
|
if rmsg.Text != "" {
|
||||||
for _, attachment := range rmsg.Extra["attachments"] {
|
return
|
||||||
attach := attachment.(map[string]interface{})
|
}
|
||||||
if attach["text"].(string) != "" {
|
|
||||||
rmsg.Text += attach["text"].(string)
|
for _, attachment := range rmsg.Extra["attachments"] {
|
||||||
continue
|
attach := attachment.(map[string]interface{})
|
||||||
}
|
if attach["text"].(string) != "" {
|
||||||
if attach["fallback"].(string) != "" {
|
rmsg.Text += attach["text"].(string)
|
||||||
rmsg.Text += attach["fallback"].(string)
|
continue
|
||||||
}
|
}
|
||||||
|
if attach["fallback"].(string) != "" {
|
||||||
|
rmsg.Text += attach["fallback"].(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
package bmattermost
|
package bmattermost
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
"github.com/42wim/matterbridge/matterclient"
|
|
||||||
"github.com/42wim/matterbridge/matterhook"
|
"github.com/42wim/matterbridge/matterhook"
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/matterbridge/matterclient"
|
||||||
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *Bmattermost) doConnectWebhookBind() error {
|
func (b *Bmattermost) doConnectWebhookBind() error {
|
||||||
@ -15,8 +16,10 @@ func (b *Bmattermost) doConnectWebhookBind() error {
|
|||||||
case b.GetString("WebhookURL") != "":
|
case b.GetString("WebhookURL") != "":
|
||||||
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
|
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
|
||||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
matterhook.Config{
|
||||||
BindAddress: b.GetString("WebhookBindAddress")})
|
InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||||
|
BindAddress: b.GetString("WebhookBindAddress"),
|
||||||
|
})
|
||||||
case b.GetString("Token") != "":
|
case b.GetString("Token") != "":
|
||||||
b.Log.Info("Connecting using token (sending)")
|
b.Log.Info("Connecting using token (sending)")
|
||||||
err := b.apiLogin()
|
err := b.apiLogin()
|
||||||
@ -32,8 +35,10 @@ func (b *Bmattermost) doConnectWebhookBind() error {
|
|||||||
default:
|
default:
|
||||||
b.Log.Info("Connecting using webhookbindaddress (receiving)")
|
b.Log.Info("Connecting using webhookbindaddress (receiving)")
|
||||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
matterhook.Config{
|
||||||
BindAddress: b.GetString("WebhookBindAddress")})
|
InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||||
|
BindAddress: b.GetString("WebhookBindAddress"),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -41,8 +46,10 @@ func (b *Bmattermost) doConnectWebhookBind() error {
|
|||||||
func (b *Bmattermost) doConnectWebhookURL() error {
|
func (b *Bmattermost) doConnectWebhookURL() error {
|
||||||
b.Log.Info("Connecting using webhookurl (sending)")
|
b.Log.Info("Connecting using webhookurl (sending)")
|
||||||
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
b.mh = matterhook.New(b.GetString("WebhookURL"),
|
||||||
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
matterhook.Config{
|
||||||
DisableServer: true})
|
InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
|
||||||
|
DisableServer: true,
|
||||||
|
})
|
||||||
if b.GetString("Token") != "" {
|
if b.GetString("Token") != "" {
|
||||||
b.Log.Info("Connecting using token (receiving)")
|
b.Log.Info("Connecting using token (receiving)")
|
||||||
err := b.apiLogin()
|
err := b.apiLogin()
|
||||||
@ -59,13 +66,14 @@ func (b *Bmattermost) doConnectWebhookURL() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:wrapcheck
|
||||||
func (b *Bmattermost) apiLogin() error {
|
func (b *Bmattermost) apiLogin() error {
|
||||||
password := b.GetString("Password")
|
password := b.GetString("Password")
|
||||||
if b.GetString("Token") != "" {
|
if b.GetString("Token") != "" {
|
||||||
password = "token=" + 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") {
|
if b.GetBool("debug") {
|
||||||
b.mc.SetLogLevel("debug")
|
b.mc.SetLogLevel("debug")
|
||||||
}
|
}
|
||||||
@ -73,14 +81,13 @@ func (b *Bmattermost) apiLogin() error {
|
|||||||
b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck")
|
b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck")
|
||||||
b.mc.NoTLS = b.GetBool("NoTLS")
|
b.mc.NoTLS = b.GetBool("NoTLS")
|
||||||
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
|
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
|
||||||
err := b.mc.Login()
|
|
||||||
if err != nil {
|
if err := b.mc.Login(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Log.Info("Connection succeeded")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,6 +120,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
|
|||||||
if b.GetBool("PrefixMessagesWithNick") {
|
if b.GetBool("PrefixMessagesWithNick") {
|
||||||
msg.Text = msg.Username + msg.Text
|
msg.Text = msg.Username + msg.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.Extra != nil {
|
if msg.Extra != nil {
|
||||||
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
|
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
|
||||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||||
@ -136,7 +144,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
|
|||||||
for _, f := range msg.Extra["file"] {
|
for _, f := range msg.Extra["file"] {
|
||||||
fi := f.(config.FileInfo)
|
fi := f.(config.FileInfo)
|
||||||
if fi.URL != "" {
|
if fi.URL != "" {
|
||||||
msg.Text += fi.URL
|
msg.Text += " " + fi.URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,19 +171,37 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// skipMessages returns true if this message should not be handled
|
// skipMessages returns true if this message should not be handled
|
||||||
|
//
|
||||||
|
//nolint:gocyclo,cyclop
|
||||||
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
|
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
|
||||||
// Handle join/leave
|
// Handle join/leave
|
||||||
if message.Type == "system_join_leave" ||
|
skipJoinMessageTypes := map[string]struct{}{
|
||||||
message.Type == "system_join_channel" ||
|
"system_join_leave": {}, // deprecated for system_add_to_channel
|
||||||
message.Type == "system_leave_channel" {
|
"system_leave_channel": {}, // deprecated for system_remove_from_channel
|
||||||
|
"system_join_channel": {},
|
||||||
|
"system_add_to_channel": {},
|
||||||
|
"system_remove_from_channel": {},
|
||||||
|
"system_add_to_team": {},
|
||||||
|
"system_remove_from_team": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// dirty hack to efficiently check if this element is in the map without writing a contains func
|
||||||
|
// can be replaced with native slice.contains with go 1.21
|
||||||
|
if _, ok := skipJoinMessageTypes[message.Type]; ok {
|
||||||
if b.GetBool("nosendjoinpart") {
|
if b.GetBool("nosendjoinpart") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channelName := b.getChannelName(message.Post.ChannelId)
|
||||||
|
if channelName == "" {
|
||||||
|
channelName = message.Channel
|
||||||
|
}
|
||||||
|
|
||||||
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
|
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
|
||||||
b.Remote <- config.Message{
|
b.Remote <- config.Message{
|
||||||
Username: "system",
|
Username: "system",
|
||||||
Text: message.Text,
|
Text: message.Text,
|
||||||
Channel: message.Channel,
|
Channel: channelName,
|
||||||
Account: b.Account,
|
Account: b.Account,
|
||||||
Event: config.EventJoinLeave,
|
Event: config.EventJoinLeave,
|
||||||
}
|
}
|
||||||
@ -183,7 +209,7 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle edited messages
|
// Handle edited messages
|
||||||
if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") {
|
if (message.Raw.EventType() == model.WebsocketEventPostEdited) && b.GetBool("EditDisable") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,13 +222,14 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
|
|||||||
// Ignore messages sent from matterbridge
|
// Ignore messages sent from matterbridge
|
||||||
if message.Post.Props != nil {
|
if message.Post.Props != nil {
|
||||||
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
|
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
|
||||||
b.Log.Debugf("sent by matterbridge, ignoring")
|
b.Log.Debug("sent by matterbridge, ignoring")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore messages sent from a user logged in as the bot
|
// Ignore messages sent from a user logged in as the bot
|
||||||
if b.mc.User.Username == message.Username {
|
if b.mc.User.Username == message.Username {
|
||||||
|
b.Log.Debug("message from same user as bot, ignoring")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,14 +239,56 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ignore messages from other teams than ours
|
// ignore messages from other teams than ours
|
||||||
if message.Raw.Data["team_id"].(string) != b.TeamID {
|
if message.Raw.GetData()["team_id"].(string) != b.TeamID {
|
||||||
|
b.Log.Debug("message from other team, ignoring")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// only handle posted, edited or deleted events
|
// only handle posted, edited or deleted events
|
||||||
if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED ||
|
if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model.WebsocketEventPostEdited ||
|
||||||
message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) {
|
message.Raw.EventType() == model.WebsocketEventPostDeleted) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) getVersion() string {
|
||||||
|
proto := "https"
|
||||||
|
|
||||||
|
if b.GetBool("notls") {
|
||||||
|
proto = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(proto + "://" + b.GetString("server"))
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Error("failed getting version")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return resp.Header.Get("X-Version-Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) getChannelID(name string) string {
|
||||||
|
idcheck := strings.Split(name, "ID:")
|
||||||
|
if len(idcheck) > 1 {
|
||||||
|
return idcheck[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.mc.GetChannelID(name, b.TeamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) getChannelName(id string) string {
|
||||||
|
b.channelsMutex.RLock()
|
||||||
|
defer b.channelsMutex.RUnlock()
|
||||||
|
|
||||||
|
for _, c := range b.channelInfoMap {
|
||||||
|
if c.Name == "ID:"+id {
|
||||||
|
// if we have ID: specified in our gateway configuration return this
|
||||||
|
return c.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
@ -1,31 +1,44 @@
|
|||||||
package bmattermost
|
package bmattermost
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/42wim/matterbridge/bridge"
|
"github.com/42wim/matterbridge/bridge"
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
"github.com/42wim/matterbridge/matterclient"
|
|
||||||
"github.com/42wim/matterbridge/matterhook"
|
"github.com/42wim/matterbridge/matterhook"
|
||||||
|
"github.com/matterbridge/matterclient"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Bmattermost struct {
|
type Bmattermost struct {
|
||||||
mh *matterhook.Client
|
mh *matterhook.Client
|
||||||
mc *matterclient.MMClient
|
mc *matterclient.Client
|
||||||
|
v6 bool
|
||||||
uuid string
|
uuid string
|
||||||
TeamID string
|
TeamID string
|
||||||
*bridge.Config
|
*bridge.Config
|
||||||
avatarMap map[string]string
|
avatarMap map[string]string
|
||||||
|
channelsMutex sync.RWMutex
|
||||||
|
channelInfoMap map[string]*config.ChannelInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
const mattermostPlugin = "mattermost.plugin"
|
const mattermostPlugin = "mattermost.plugin"
|
||||||
|
|
||||||
func New(cfg *bridge.Config) bridge.Bridger {
|
func New(cfg *bridge.Config) bridge.Bridger {
|
||||||
b := &Bmattermost{Config: cfg, avatarMap: make(map[string]string)}
|
b := &Bmattermost{
|
||||||
|
Config: cfg,
|
||||||
|
avatarMap: make(map[string]string),
|
||||||
|
channelInfoMap: make(map[string]*config.ChannelInfo),
|
||||||
|
}
|
||||||
|
|
||||||
|
b.v6 = b.GetBool("v6")
|
||||||
b.uuid = xid.New().String()
|
b.uuid = xid.New().String()
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,6 +50,13 @@ func (b *Bmattermost) Connect() error {
|
|||||||
if b.Account == mattermostPlugin {
|
if b.Account == mattermostPlugin {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(b.getVersion(), "6.") || strings.HasPrefix(b.getVersion(), "7.") {
|
||||||
|
if !b.v6 {
|
||||||
|
b.v6 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if b.GetString("WebhookBindAddress") != "" {
|
if b.GetString("WebhookBindAddress") != "" {
|
||||||
if err := b.doConnectWebhookBind(); err != nil {
|
if err := b.doConnectWebhookBind(); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -60,6 +80,7 @@ func (b *Bmattermost) Connect() error {
|
|||||||
go b.handleMatter()
|
go b.handleMatter()
|
||||||
case b.GetString("Login") != "":
|
case b.GetString("Login") != "":
|
||||||
b.Log.Info("Connecting using login/password (sending and receiving)")
|
b.Log.Info("Connecting using login/password (sending and receiving)")
|
||||||
|
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
|
||||||
err := b.apiLogin()
|
err := b.apiLogin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -81,14 +102,21 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
|
|||||||
if b.Account == mattermostPlugin {
|
if b.Account == mattermostPlugin {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.channelsMutex.Lock()
|
||||||
|
b.channelInfoMap[channel.ID] = &channel
|
||||||
|
b.channelsMutex.Unlock()
|
||||||
|
|
||||||
// we can only join channels using the API
|
// we can only join channels using the API
|
||||||
if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" {
|
if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" {
|
||||||
id := b.mc.GetChannelId(channel.Name, b.TeamID)
|
id := b.getChannelID(channel.Name)
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
|
return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.mc.JoinChannel(id)
|
return b.mc.JoinChannel(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,19 +146,31 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
|
|||||||
if msg.ID == "" {
|
if msg.ID == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg.ID, b.mc.DeleteMessage(msg.ID)
|
return msg.ID, b.mc.DeleteMessage(msg.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle prefix hint for unthreaded messages.
|
// Handle prefix hint for unthreaded messages.
|
||||||
if msg.ParentID == "msg-parent-not-found" {
|
if msg.ParentNotFound() {
|
||||||
msg.ParentID = ""
|
msg.ParentID = ""
|
||||||
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
|
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we only can reply to the root of the thread, not to a specific ID (like discord for example does)
|
||||||
|
if msg.ParentID != "" {
|
||||||
|
post, _, err := b.mc.Client.GetPost(context.TODO(), msg.ParentID, "")
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
|
||||||
|
}
|
||||||
|
if post != nil && post.RootId != "" {
|
||||||
|
msg.ParentID = post.RootId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Upload a file if it exists
|
// Upload a file if it exists
|
||||||
if msg.Extra != nil {
|
if msg.Extra != nil {
|
||||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||||
if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
|
if _, err := b.mc.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
|
||||||
b.Log.Errorf("PostMessage failed: %s", err)
|
b.Log.Errorf("PostMessage failed: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,5 +190,5 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Post normal message
|
// Post normal message
|
||||||
return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID)
|
return b.mc.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID)
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,10 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"}
|
var (
|
||||||
var attachRE = regexp.MustCompile(`<attachment id=.*?attachment>`)
|
defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"}
|
||||||
|
attachRE = regexp.MustCompile(`<attachment id=.*?attachment>`)
|
||||||
|
)
|
||||||
|
|
||||||
type Bmsteams struct {
|
type Bmsteams struct {
|
||||||
gc *msgraph.GraphServiceRequestBuilder
|
gc *msgraph.GraphServiceRequestBuilder
|
||||||
@ -50,7 +52,7 @@ func (b *Bmsteams) Connect() error {
|
|||||||
b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err)
|
b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err)
|
||||||
}
|
}
|
||||||
// make file readable only for matterbridge user
|
// make file readable only for matterbridge user
|
||||||
err = os.Chmod(tokenCachePath, 0600)
|
err = os.Chmod(tokenCachePath, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err)
|
b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err)
|
||||||
}
|
}
|
||||||
@ -86,13 +88,16 @@ func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error {
|
|||||||
|
|
||||||
func (b *Bmsteams) Send(msg config.Message) (string, error) {
|
func (b *Bmsteams) Send(msg config.Message) (string, error) {
|
||||||
b.Log.Debugf("=> Receiving %#v", msg)
|
b.Log.Debugf("=> Receiving %#v", msg)
|
||||||
if msg.ParentID != "" && msg.ParentID != "msg-parent-not-found" {
|
if msg.ParentValid() {
|
||||||
return b.sendReply(msg)
|
return b.sendReply(msg)
|
||||||
}
|
}
|
||||||
if msg.ParentID == "msg-parent-not-found" {
|
|
||||||
|
// Handle prefix hint for unthreaded messages.
|
||||||
|
if msg.ParentNotFound() {
|
||||||
msg.ParentID = ""
|
msg.ParentID = ""
|
||||||
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
|
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request()
|
ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request()
|
||||||
text := msg.Username + msg.Text
|
text := msg.Username + msg.Text
|
||||||
content := &msgraph.ItemBody{Content: &text}
|
content := &msgraph.ItemBody{Content: &text}
|
||||||
@ -165,7 +170,7 @@ func (b *Bmsteams) poll(channelName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// skip non-user message for now.
|
// skip non-user message for now.
|
||||||
if msg.From.User == nil {
|
if msg.From == nil || msg.From.User == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
70
bridge/mumble/codec.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package bmumble
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"layeh.com/gumble/gumble"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a dummy implementation of a Gumble audio codec which claims
|
||||||
|
// to implement Opus, but does not actually do anything. This serves
|
||||||
|
// as a workaround until https://github.com/layeh/gumble/pull/61 is
|
||||||
|
// merged.
|
||||||
|
// See https://github.com/42wim/matterbridge/issues/1750 for details.
|
||||||
|
|
||||||
|
const (
|
||||||
|
audioCodecIDOpus = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerNullCodecAsOpus() {
|
||||||
|
codec := &NullCodec{
|
||||||
|
encoder: &NullAudioEncoder{},
|
||||||
|
decoder: &NullAudioDecoder{},
|
||||||
|
}
|
||||||
|
gumble.RegisterAudioCodec(audioCodecIDOpus, codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullCodec struct {
|
||||||
|
encoder *NullAudioEncoder
|
||||||
|
decoder *NullAudioDecoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *NullCodec) ID() int {
|
||||||
|
return audioCodecIDOpus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *NullCodec) NewEncoder() gumble.AudioEncoder {
|
||||||
|
e := &NullAudioEncoder{}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *NullCodec) NewDecoder() gumble.AudioDecoder {
|
||||||
|
d := &NullAudioDecoder{}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullAudioEncoder struct{}
|
||||||
|
|
||||||
|
func (e *NullAudioEncoder) ID() int {
|
||||||
|
return audioCodecIDOpus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NullAudioEncoder) Encode(pcm []int16, mframeSize, maxDataBytes int) ([]byte, error) {
|
||||||
|
return nil, fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NullAudioEncoder) Reset() {
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullAudioDecoder struct{}
|
||||||
|
|
||||||
|
func (d *NullAudioDecoder) ID() int {
|
||||||
|
return audioCodecIDOpus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *NullAudioDecoder) Decode(data []byte, frameSize int) ([]int16, error) {
|
||||||
|
return nil, fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *NullAudioDecoder) Reset() {
|
||||||
|
}
|
@ -19,6 +19,12 @@ func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
|
|||||||
if event.TextMessage.Sender != nil {
|
if event.TextMessage.Sender != nil {
|
||||||
sender = event.TextMessage.Sender.Name
|
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
|
// Convert Mumble HTML messages to markdown
|
||||||
parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message)
|
parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -36,7 +42,14 @@ func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
|
|||||||
if part.Image == nil {
|
if part.Image == nil {
|
||||||
rmsg.Text = part.Text
|
rmsg.Text = part.Text
|
||||||
} else {
|
} else {
|
||||||
fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + part.FileExtension
|
fileExt := part.FileExtension
|
||||||
|
if fileExt == ".jfif" {
|
||||||
|
fileExt = ".jpg"
|
||||||
|
}
|
||||||
|
if fileExt == ".jpe" {
|
||||||
|
fileExt = ".jpg"
|
||||||
|
}
|
||||||
|
fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + fileExt
|
||||||
rmsg.Extra = make(map[string][]interface{})
|
rmsg.Extra = make(map[string][]interface{})
|
||||||
if err = helper.HandleDownloadSize(b.Log, &rmsg, fname, int64(len(part.Image)), b.General); err != nil {
|
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")
|
b.Log.WithError(err).Warn("not including image in message")
|
||||||
@ -56,7 +69,6 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
|
|||||||
}
|
}
|
||||||
// No need to talk or listen
|
// No need to talk or listen
|
||||||
event.Client.Self.SetSelfDeafened(true)
|
event.Client.Self.SetSelfDeafened(true)
|
||||||
event.Client.Self.SetSelfMuted(true)
|
|
||||||
// if the Channel variable is set, this is a reconnect -> rejoin channel
|
// if the Channel variable is set, this is a reconnect -> rejoin channel
|
||||||
if b.Channel != nil {
|
if b.Channel != nil {
|
||||||
if err := b.doJoin(event.Client, *b.Channel); err != nil {
|
if err := b.doJoin(event.Client, *b.Channel); err != nil {
|
||||||
@ -72,19 +84,75 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) {
|
func (b *Bmumble) handleJoinLeave(event *gumble.UserChangeEvent) {
|
||||||
// Only care about changes to self
|
// Ignore events happening before setup is done
|
||||||
if event.User != event.Client.Self {
|
if b.Channel == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Someone attempted to move the user out of the configured channel; attempt to join back
|
if b.GetBool("nosendjoinpart") {
|
||||||
if b.Channel != nil {
|
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 {
|
if err := b.doJoin(event.Client, *b.Channel); err != nil {
|
||||||
b.Log.Error(err)
|
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) {
|
func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) {
|
||||||
b.connected <- *event
|
b.connected <- *event
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"layeh.com/gumble/gumble"
|
"layeh.com/gumble/gumble"
|
||||||
@ -92,7 +93,7 @@ func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
|
|||||||
func (b *Bmumble) Send(msg config.Message) (string, error) {
|
func (b *Bmumble) Send(msg config.Message) (string, error) {
|
||||||
// Only process text messages
|
// Only process text messages
|
||||||
b.Log.Debugf("=> Received local message %#v", msg)
|
b.Log.Debugf("=> Received local message %#v", msg)
|
||||||
if msg.Event != "" && msg.Event != config.EventUserAction {
|
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,6 +185,7 @@ func (b *Bmumble) doConnect() error {
|
|||||||
gumbleConfig.Password = password
|
gumbleConfig.Password = password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerNullCodecAsOpus()
|
||||||
client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
|
client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -248,12 +250,19 @@ func (b *Bmumble) processMessage(msg *config.Message) {
|
|||||||
// If there is a maximum message length, split and truncate the lines
|
// If there is a maximum message length, split and truncate the lines
|
||||||
var msgLines []string
|
var msgLines []string
|
||||||
if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
|
if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
|
||||||
msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username))
|
if *maxLength != 0 { // Some servers will have unlimited message lengths.
|
||||||
|
// Not doing this makes underflows happen.
|
||||||
|
msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
|
||||||
|
} else {
|
||||||
|
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
msgLines = helper.GetSubLines(msg.Text, 0)
|
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
|
||||||
}
|
}
|
||||||
// Send the individual lindes
|
// Send the individual lines
|
||||||
for i := range msgLines {
|
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)
|
b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,44 +74,33 @@ func (b *Btalk) JoinChannel(channel config.ChannelInfo) error {
|
|||||||
}
|
}
|
||||||
b.rooms = append(b.rooms, newRoom)
|
b.rooms = append(b.rooms, newRoom)
|
||||||
|
|
||||||
// Config
|
|
||||||
guestSuffix := " (Guest)"
|
|
||||||
if b.IsKeySet("GuestSuffix") {
|
|
||||||
guestSuffix = b.GetString("GuestSuffix")
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for msg := range c {
|
for msg := range c {
|
||||||
msg := msg
|
msg := msg
|
||||||
// ignore messages that are one of the following
|
|
||||||
// * not a message from a user
|
if msg.Error != nil {
|
||||||
// * from ourselves
|
b.Log.Errorf("Fatal message poll error: %s\n", msg.Error)
|
||||||
if msg.MessageType != ocs.MessageComment || msg.ActorID == b.user.User {
|
|
||||||
continue
|
return
|
||||||
}
|
|
||||||
remoteMessage := config.Message{
|
|
||||||
Text: formatRichObjectString(msg.Message, msg.MessageParameters),
|
|
||||||
Channel: newRoom.room.Token,
|
|
||||||
Username: DisplayName(msg, guestSuffix),
|
|
||||||
UserID: msg.ActorID,
|
|
||||||
Account: b.Account,
|
|
||||||
}
|
|
||||||
// It is possible for the ID to not be set on older versions of Talk so we only set it if
|
|
||||||
// the ID is not blank
|
|
||||||
if msg.ID != 0 {
|
|
||||||
remoteMessage.ID = strconv.Itoa(msg.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Files
|
// Ignore messages that are from the bot user
|
||||||
err = b.handleFiles(&remoteMessage, &msg)
|
if msg.ActorID == b.user.User || msg.ActorType == "bridged" {
|
||||||
if err != nil {
|
continue
|
||||||
b.Log.Errorf("Error handling file: %#v", msg)
|
}
|
||||||
|
|
||||||
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Log.Debugf("<= Message is %#v", remoteMessage)
|
|
||||||
b.Remote <- remoteMessage
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
@ -124,16 +113,40 @@ func (b *Btalk) Send(msg config.Message) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Talk currently only supports sending normal messages
|
// Standard Message Send
|
||||||
if msg.Event != "" {
|
if msg.Event == "" {
|
||||||
return "", nil
|
// Handle sending files if they are included
|
||||||
|
err := b.handleSendingFile(&msg, r)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Could not send files in message to room %v from %v: %v", msg.Channel, msg.Username, err)
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sentMessage, err := b.sendText(r, &msg, msg.Text)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err)
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return strconv.Itoa(sentMessage.ID), nil
|
||||||
}
|
}
|
||||||
sentMessage, err := r.room.SendMessage(msg.Username + msg.Text)
|
|
||||||
if err != nil {
|
// Message Deletion
|
||||||
b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err)
|
if msg.Event == config.EventMsgDelete {
|
||||||
return "", nil
|
messageID, err := strconv.Atoi(msg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
data, err := r.room.DeleteMessage(messageID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strconv.Itoa(data.ID), nil
|
||||||
}
|
}
|
||||||
return strconv.Itoa(sentMessage.ID), nil
|
|
||||||
|
// Message is not a type that is currently supported
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Btalk) getRoom(token string) *Broom {
|
func (b *Btalk) getRoom(token string) *Broom {
|
||||||
@ -145,6 +158,17 @@ func (b *Btalk) getRoom(token string) *Broom {
|
|||||||
return nil
|
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 {
|
func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error {
|
||||||
for _, parameter := range message.MessageParameters {
|
for _, parameter := range message.MessageParameters {
|
||||||
if parameter.Type == ocs.ROSTypeFile {
|
if parameter.Type == ocs.ROSTypeFile {
|
||||||
@ -170,6 +194,74 @@ func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageDa
|
|||||||
return nil
|
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
|
// Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785
|
||||||
func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string {
|
func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string {
|
||||||
for id, parameter := range parameters {
|
for id, parameter := range parameters {
|
||||||
@ -190,7 +282,7 @@ func formatRichObjectString(message string, parameters map[string]ocs.RichObject
|
|||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
func DisplayName(msg ocs.TalkRoomMessageData, suffix string) string {
|
func DisplayName(msg *ocs.TalkRoomMessageData, suffix string) string {
|
||||||
if msg.ActorType == ocs.ActorGuest {
|
if msg.ActorType == ocs.ActorGuest {
|
||||||
if msg.ActorDisplayName == "" {
|
if msg.ActorDisplayName == "" {
|
||||||
return "Guest"
|
return "Guest"
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package brocketchat
|
package brocketchat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
|
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -58,6 +61,7 @@ func (b *Brocketchat) handleStatusEvent(ev models.Message, rmsg *config.Message)
|
|||||||
|
|
||||||
func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
|
func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
|
||||||
for message := range b.messageChan {
|
for message := range b.messageChan {
|
||||||
|
message := message
|
||||||
// skip messages with same ID, apparently messages get duplicated for an unknown reason
|
// skip messages with same ID, apparently messages get duplicated for an unknown reason
|
||||||
if _, ok := b.cache.Get(message.ID); ok {
|
if _, ok := b.cache.Get(message.ID); ok {
|
||||||
continue
|
continue
|
||||||
@ -76,8 +80,11 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
|
|||||||
Account: b.Account,
|
Account: b.Account,
|
||||||
UserID: message.User.ID,
|
UserID: message.User.ID,
|
||||||
ID: message.ID,
|
ID: message.ID,
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.handleAttachments(&message, rmsg)
|
||||||
|
|
||||||
// handleStatusEvent returns false if the message should be dropped
|
// 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
|
// in that case it is probably some modification to the channel we do not want to relay
|
||||||
if b.handleStatusEvent(m, rmsg) {
|
if b.handleStatusEvent(m, rmsg) {
|
||||||
@ -86,6 +93,38 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Brocketchat) handleAttachments(message *models.Message, rmsg *config.Message) {
|
||||||
|
if rmsg.Text == "" {
|
||||||
|
for _, attachment := range message.Attachments {
|
||||||
|
if attachment.Title != "" {
|
||||||
|
rmsg.Text = attachment.Title + "\n"
|
||||||
|
}
|
||||||
|
if attachment.Title != "" && attachment.Text != "" {
|
||||||
|
rmsg.Text += "\n"
|
||||||
|
}
|
||||||
|
if attachment.Text != "" {
|
||||||
|
rmsg.Text += attachment.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range message.Attachments {
|
||||||
|
if err := b.handleDownloadFile(rmsg, &message.Attachments[i]); err != nil {
|
||||||
|
b.Log.Errorf("Could not download incoming file: %#v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brocketchat) handleDownloadFile(rmsg *config.Message, file *models.Attachment) error {
|
||||||
|
downloadURL := b.GetString("server") + file.TitleLink
|
||||||
|
data, err := helper.DownloadFileAuthRocket(downloadURL, b.user.Token, b.user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("download %s failed %#v", downloadURL, err)
|
||||||
|
}
|
||||||
|
helper.HandleDownloadData(b.Log, rmsg, file.Title, rmsg.Text, downloadURL, data, b.General)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Brocketchat) handleUploadFile(msg *config.Message) error {
|
func (b *Brocketchat) handleUploadFile(msg *config.Message) error {
|
||||||
for _, f := range msg.Extra["file"] {
|
for _, f := range msg.Extra["file"] {
|
||||||
fi := f.(config.FileInfo)
|
fi := f.(config.FileInfo)
|
||||||
|
@ -135,6 +135,7 @@ func (b *Brocketchat) uploadFile(fi *config.FileInfo, channel string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -27,7 +27,8 @@ func (b *Bslack) handleSlack() {
|
|||||||
b.Log.Debug("Start listening for Slack messages")
|
b.Log.Debug("Start listening for Slack messages")
|
||||||
for message := range messages {
|
for message := range messages {
|
||||||
// don't do any action on deleted/typing messages
|
// don't do any action on deleted/typing messages
|
||||||
if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete {
|
if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete &&
|
||||||
|
message.Event != config.EventFileDelete {
|
||||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
|
||||||
// cleanup the message
|
// cleanup the message
|
||||||
message.Text = b.replaceMention(message.Text)
|
message.Text = b.replaceMention(message.Text)
|
||||||
@ -76,6 +77,13 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
messages <- rmsg
|
messages <- rmsg
|
||||||
|
case *slack.FileDeletedEvent:
|
||||||
|
rmsg, err := b.handleFileDeletedEvent(ev)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Printf("%#v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
messages <- rmsg
|
||||||
case *slack.OutgoingErrorEvent:
|
case *slack.OutgoingErrorEvent:
|
||||||
b.Log.Debugf("%#v", ev.Error())
|
b.Log.Debugf("%#v", ev.Error())
|
||||||
case *slack.ChannelJoinedEvent:
|
case *slack.ChannelJoinedEvent:
|
||||||
@ -95,6 +103,8 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
|
|||||||
b.users.populateUser(ev.User)
|
b.users.populateUser(ev.User)
|
||||||
case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent:
|
case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent:
|
||||||
continue
|
continue
|
||||||
|
case *slack.UserChangeEvent:
|
||||||
|
b.users.invalidateUser(ev.User.ID)
|
||||||
default:
|
default:
|
||||||
b.Log.Debugf("Unhandled incoming event: %T", ev)
|
b.Log.Debugf("Unhandled incoming event: %T", ev)
|
||||||
}
|
}
|
||||||
@ -220,6 +230,26 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
|
|||||||
return rmsg, nil
|
return rmsg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) handleFileDeletedEvent(ev *slack.FileDeletedEvent) (*config.Message, error) {
|
||||||
|
if rawChannel, ok := b.cache.Get(cfileDownloadChannel + ev.FileID); ok {
|
||||||
|
channel, err := b.channels.getChannelByID(rawChannel.(string))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config.Message{
|
||||||
|
Event: config.EventFileDelete,
|
||||||
|
Text: config.EventFileDelete,
|
||||||
|
Channel: channel.Name,
|
||||||
|
Account: b.Account,
|
||||||
|
ID: ev.FileID,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("channel ID for file ID %s not found", ev.FileID)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
|
func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
|
||||||
switch ev.SubType {
|
switch ev.SubType {
|
||||||
case sChannelJoined, sMemberJoined:
|
case sChannelJoined, sMemberJoined:
|
||||||
@ -252,6 +282,13 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
|
|||||||
return false
|
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) {
|
func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) {
|
||||||
// File comments are set by the system (because there is no username given).
|
// File comments are set by the system (because there is no username given).
|
||||||
if ev.SubType == sFileComment {
|
if ev.SubType == sFileComment {
|
||||||
@ -260,12 +297,15 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
|
|||||||
|
|
||||||
// See if we have some text in the attachments.
|
// See if we have some text in the attachments.
|
||||||
if rmsg.Text == "" {
|
if rmsg.Text == "" {
|
||||||
for _, attach := range ev.Attachments {
|
for i, attach := range ev.Attachments {
|
||||||
if attach.Text != "" {
|
if attach.Text != "" {
|
||||||
if attach.Title != "" {
|
if attach.Title != "" {
|
||||||
rmsg.Text = attach.Title + "\n"
|
rmsg.Text = getMessageTitle(&ev.Attachments[i])
|
||||||
}
|
}
|
||||||
rmsg.Text += attach.Text
|
rmsg.Text += attach.Text
|
||||||
|
if attach.Footer != "" {
|
||||||
|
rmsg.Text += "\n\n" + attach.Footer
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
rmsg.Text = attach.Fallback
|
rmsg.Text = attach.Fallback
|
||||||
}
|
}
|
||||||
@ -279,6 +319,8 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
|
|||||||
|
|
||||||
// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
|
// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
|
||||||
for i := range ev.Files {
|
for i := range ev.Files {
|
||||||
|
// keep reference in cache on which channel we added this file
|
||||||
|
b.cache.Add(cfileDownloadChannel+ev.Files[i].ID, ev.Channel)
|
||||||
if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
|
if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
|
||||||
b.Log.Errorf("Could not download incoming file: %#v", err)
|
b.Log.Errorf("Could not download incoming file: %#v", err)
|
||||||
}
|
}
|
||||||
@ -328,7 +370,7 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retr
|
|||||||
// that the comment is not duplicated.
|
// that the comment is not duplicated.
|
||||||
comment := rmsg.Text
|
comment := rmsg.Text
|
||||||
rmsg.Text = ""
|
rmsg.Text = ""
|
||||||
helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General)
|
helper.HandleDownloadData2(b.Log, rmsg, file.Name, file.ID, comment, file.URLPrivateDownload, data, b.General)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +87,9 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi
|
|||||||
if user.Profile.DisplayName != "" {
|
if user.Profile.DisplayName != "" {
|
||||||
rmsg.Username = user.Profile.DisplayName
|
rmsg.Username = user.Profile.DisplayName
|
||||||
}
|
}
|
||||||
|
if b.GetBool("UseFullName") && user.Profile.RealName != "" {
|
||||||
|
rmsg.Username = user.Profile.RealName
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +101,9 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
|
|||||||
var err error
|
var err error
|
||||||
var bot *slack.Bot
|
var bot *slack.Bot
|
||||||
for {
|
for {
|
||||||
bot, err = b.rtm.GetBotInfo(ev.BotID)
|
bot, err = b.rtm.GetBotInfo(slack.GetBotInfoParameters{
|
||||||
|
Bot: ev.BotID,
|
||||||
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -124,7 +129,7 @@ var (
|
|||||||
mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
|
mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
|
||||||
channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
|
channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
|
||||||
variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
|
variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
|
||||||
urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`)
|
urlRE = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
|
||||||
codeFenceRE = regexp.MustCompile(`(?m)^` + "```" + `\w+$`)
|
codeFenceRE = regexp.MustCompile(`(?m)^` + "```" + `\w+$`)
|
||||||
topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`)
|
topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`)
|
||||||
)
|
)
|
||||||
@ -178,14 +183,7 @@ func (b *Bslack) replaceVariable(text string) string {
|
|||||||
|
|
||||||
// @see https://api.slack.com/docs/message-formatting#linking_to_urls
|
// @see https://api.slack.com/docs/message-formatting#linking_to_urls
|
||||||
func (b *Bslack) replaceURL(text string) string {
|
func (b *Bslack) replaceURL(text string) string {
|
||||||
for _, r := range urlRE.FindAllStringSubmatch(text, -1) {
|
return urlRE.ReplaceAllString(text, "[${2}](${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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bslack) replaceb0rkedMarkDown(text string) string {
|
func (b *Bslack) replaceb0rkedMarkDown(text string) string {
|
||||||
|
@ -36,24 +36,25 @@ type Bslack struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
sHello = "hello"
|
sHello = "hello"
|
||||||
sChannelJoin = "channel_join"
|
sChannelJoin = "channel_join"
|
||||||
sChannelLeave = "channel_leave"
|
sChannelLeave = "channel_leave"
|
||||||
sChannelJoined = "channel_joined"
|
sChannelJoined = "channel_joined"
|
||||||
sMemberJoined = "member_joined_channel"
|
sMemberJoined = "member_joined_channel"
|
||||||
sMessageChanged = "message_changed"
|
sMessageChanged = "message_changed"
|
||||||
sMessageDeleted = "message_deleted"
|
sMessageDeleted = "message_deleted"
|
||||||
sSlackAttachment = "slack_attachment"
|
sSlackAttachment = "slack_attachment"
|
||||||
sPinnedItem = "pinned_item"
|
sPinnedItem = "pinned_item"
|
||||||
sUnpinnedItem = "unpinned_item"
|
sUnpinnedItem = "unpinned_item"
|
||||||
sChannelTopic = "channel_topic"
|
sChannelTopic = "channel_topic"
|
||||||
sChannelPurpose = "channel_purpose"
|
sChannelPurpose = "channel_purpose"
|
||||||
sFileComment = "file_comment"
|
sFileComment = "file_comment"
|
||||||
sMeMessage = "me_message"
|
sMeMessage = "me_message"
|
||||||
sUserTyping = "user_typing"
|
sUserTyping = "user_typing"
|
||||||
sLatencyReport = "latency_report"
|
sLatencyReport = "latency_report"
|
||||||
sSystemUser = "system"
|
sSystemUser = "system"
|
||||||
sSlackBotUser = "slackbot"
|
sSlackBotUser = "slackbot"
|
||||||
|
cfileDownloadChannel = "file_download_channel"
|
||||||
|
|
||||||
tokenConfig = "Token"
|
tokenConfig = "Token"
|
||||||
incomingWebhookConfig = "WebhookBindAddress"
|
incomingWebhookConfig = "WebhookBindAddress"
|
||||||
@ -156,7 +157,7 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
|
|||||||
|
|
||||||
// try to join a channel when in legacy
|
// try to join a channel when in legacy
|
||||||
if b.legacy {
|
if b.legacy {
|
||||||
_, err := b.sc.JoinChannel(channel.Name)
|
_, _, _, err := b.sc.JoinConversation(channel.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err.Error() {
|
switch err.Error() {
|
||||||
case "name_taken", "restricted_action":
|
case "name_taken", "restricted_action":
|
||||||
@ -195,7 +196,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
|
|||||||
b.Log.Debugf("=> Receiving %#v", msg)
|
b.Log.Debugf("=> Receiving %#v", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.Text = helper.ClipMessage(msg.Text, messageLength)
|
msg.Text = helper.ClipMessage(msg.Text, messageLength, b.GetString("MessageClipped"))
|
||||||
msg.Text = b.replaceCodeFence(msg.Text)
|
msg.Text = b.replaceCodeFence(msg.Text)
|
||||||
|
|
||||||
// Make a action /me of the message
|
// Make a action /me of the message
|
||||||
@ -299,7 +300,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle prefix hint for unthreaded messages.
|
// Handle prefix hint for unthreaded messages.
|
||||||
if msg.ParentID == "msg-parent-not-found" {
|
if msg.ParentNotFound() {
|
||||||
msg.ParentID = ""
|
msg.ParentID = ""
|
||||||
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
|
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
|
||||||
}
|
}
|
||||||
@ -320,7 +321,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upload a file if it exists.
|
// Upload a file if it exists.
|
||||||
if msg.Extra != nil {
|
if len(msg.Extra) > 0 {
|
||||||
extraMsgs := helper.HandleExtra(&msg, b.General)
|
extraMsgs := helper.HandleExtra(&msg, b.General)
|
||||||
for i := range extraMsgs {
|
for i := range extraMsgs {
|
||||||
rmsg := &extraMsgs[i]
|
rmsg := &extraMsgs[i]
|
||||||
@ -331,7 +332,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Upload files if necessary (from Slack, Telegram or Mattermost).
|
// Upload files if necessary (from Slack, Telegram or Mattermost).
|
||||||
b.uploadFile(&msg, channelInfo.ID)
|
return b.uploadFile(&msg, channelInfo.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post message.
|
// Post message.
|
||||||
@ -442,7 +443,8 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// uploadFile handles native upload of files
|
// uploadFile handles native upload of files
|
||||||
func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
|
func (b *Bslack) uploadFile(msg *config.Message, channelID string) (string, error) {
|
||||||
|
var messageID string
|
||||||
for _, f := range msg.Extra["file"] {
|
for _, f := range msg.Extra["file"] {
|
||||||
fi, ok := f.(config.FileInfo)
|
fi, ok := f.(config.FileInfo)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -459,7 +461,7 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
|
|||||||
b.cache.Add("filename"+fi.Name, ts)
|
b.cache.Add("filename"+fi.Name, ts)
|
||||||
initialComment := fmt.Sprintf("File from %s", msg.Username)
|
initialComment := fmt.Sprintf("File from %s", msg.Username)
|
||||||
if fi.Comment != "" {
|
if fi.Comment != "" {
|
||||||
initialComment += fmt.Sprintf("with comment: %s", fi.Comment)
|
initialComment += fmt.Sprintf(" with comment: %s", fi.Comment)
|
||||||
}
|
}
|
||||||
res, err := b.sc.UploadFile(slack.FileUploadParameters{
|
res, err := b.sc.UploadFile(slack.FileUploadParameters{
|
||||||
Reader: bytes.NewReader(*fi.Data),
|
Reader: bytes.NewReader(*fi.Data),
|
||||||
@ -470,13 +472,22 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("uploadfile %#v", err)
|
b.Log.Errorf("uploadfile %#v", err)
|
||||||
return
|
return "", err
|
||||||
}
|
}
|
||||||
if res.ID != "" {
|
if res.ID != "" {
|
||||||
b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String())
|
b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String())
|
||||||
b.cache.Add("file"+res.ID, ts)
|
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 {
|
func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
|
||||||
|
@ -113,6 +113,12 @@ func (b *users) populateUser(userID string) {
|
|||||||
b.users[userID] = user
|
b.users[userID] = user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *users) invalidateUser(userID string) {
|
||||||
|
b.usersMutex.Lock()
|
||||||
|
defer b.usersMutex.Unlock()
|
||||||
|
delete(b.users, userID)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *users) populateUsers(wait bool) {
|
func (b *users) populateUsers(wait bool) {
|
||||||
b.refreshMutex.Lock()
|
b.refreshMutex.Lock()
|
||||||
if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
|
if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
|
||||||
@ -283,8 +289,9 @@ func (b *channels) populateChannels(wait bool) {
|
|||||||
// We only retrieve public and private channels, not IMs
|
// We only retrieve public and private channels, not IMs
|
||||||
// and MPIMs as those do not have a channel name.
|
// and MPIMs as those do not have a channel name.
|
||||||
queryParams := &slack.GetConversationsParameters{
|
queryParams := &slack.GetConversationsParameters{
|
||||||
ExcludeArchived: "true",
|
ExcludeArchived: true,
|
||||||
Types: []string{"public_channel,private_channel"},
|
Types: []string{"public_channel,private_channel"},
|
||||||
|
Limit: 1000,
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
channels, nextCursor, err := b.sc.GetConversations(queryParams)
|
channels, nextCursor, err := b.sc.GetConversations(queryParams)
|
||||||
|
@ -1,22 +1,41 @@
|
|||||||
package btelegram
|
package btelegram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"regexp"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
|
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
|
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
|
||||||
// handle channels
|
// handle channels
|
||||||
if posted != nil {
|
if posted != nil {
|
||||||
message = posted
|
if posted.Text == "/chatId" {
|
||||||
rmsg.Text = message.Text
|
chatID := strconv.FormatInt(posted.Chat.ID, 10)
|
||||||
|
|
||||||
|
// Handle chat topics
|
||||||
|
if posted.IsTopicMessage {
|
||||||
|
chatID = chatID + "/" + strconv.Itoa(posted.MessageThreadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := b.Send(config.Message{
|
||||||
|
Channel: chatID,
|
||||||
|
Text: fmt.Sprintf("ID of this chat: %s", chatID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Warnf("Unable to send chatID to %s", chatID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message = posted
|
||||||
|
rmsg.Text = message.Text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// edited channel message
|
// edited channel message
|
||||||
@ -43,6 +62,11 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if message.ForwardFromChat != nil && message.ForwardFrom == nil {
|
||||||
|
rmsg.Text = "Forwarded from " + message.ForwardFromChat.Title + ": " + rmsg.Text
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if message.ForwardFrom == nil {
|
if message.ForwardFrom == nil {
|
||||||
rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text
|
rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text
|
||||||
return
|
return
|
||||||
@ -52,6 +76,9 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
|
|||||||
if b.GetBool("UseFirstName") {
|
if b.GetBool("UseFirstName") {
|
||||||
usernameForward = message.ForwardFrom.FirstName
|
usernameForward = message.ForwardFrom.FirstName
|
||||||
}
|
}
|
||||||
|
if b.GetBool("UseFullName") {
|
||||||
|
usernameForward = message.ForwardFrom.FirstName + " " + message.ForwardFrom.LastName
|
||||||
|
}
|
||||||
|
|
||||||
if usernameForward == "" {
|
if usernameForward == "" {
|
||||||
usernameForward = message.ForwardFrom.UserName
|
usernameForward = message.ForwardFrom.UserName
|
||||||
@ -69,12 +96,16 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
|
|||||||
|
|
||||||
// handleQuoting handles quoting of previous messages
|
// handleQuoting handles quoting of previous messages
|
||||||
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
|
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
|
||||||
if message.ReplyToMessage != nil {
|
// Used to check if the message was a reply to the root topic
|
||||||
|
if message.ReplyToMessage != nil && (!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) { //nolint:nestif
|
||||||
usernameReply := ""
|
usernameReply := ""
|
||||||
if message.ReplyToMessage.From != nil {
|
if message.ReplyToMessage.From != nil {
|
||||||
if b.GetBool("UseFirstName") {
|
if b.GetBool("UseFirstName") {
|
||||||
usernameReply = message.ReplyToMessage.From.FirstName
|
usernameReply = message.ReplyToMessage.From.FirstName
|
||||||
}
|
}
|
||||||
|
if b.GetBool("UseFullName") {
|
||||||
|
usernameReply = message.ReplyToMessage.From.FirstName + " " + message.ReplyToMessage.From.LastName
|
||||||
|
}
|
||||||
if usernameReply == "" {
|
if usernameReply == "" {
|
||||||
usernameReply = message.ReplyToMessage.From.UserName
|
usernameReply = message.ReplyToMessage.From.UserName
|
||||||
if usernameReply == "" {
|
if usernameReply == "" {
|
||||||
@ -86,7 +117,11 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
|
|||||||
usernameReply = unknownUser
|
usernameReply = unknownUser
|
||||||
}
|
}
|
||||||
if !b.GetBool("QuoteDisable") {
|
if !b.GetBool("QuoteDisable") {
|
||||||
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
|
quote := message.ReplyToMessage.Text
|
||||||
|
if quote == "" {
|
||||||
|
quote = message.ReplyToMessage.Caption
|
||||||
|
}
|
||||||
|
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, quote)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,10 +129,15 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
|
|||||||
// handleUsername handles the correct setting of the username
|
// handleUsername handles the correct setting of the username
|
||||||
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
|
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
|
||||||
if message.From != nil {
|
if message.From != nil {
|
||||||
rmsg.UserID = strconv.Itoa(message.From.ID)
|
rmsg.UserID = strconv.FormatInt(message.From.ID, 10)
|
||||||
if b.GetBool("UseFirstName") {
|
if b.GetBool("UseFirstName") {
|
||||||
rmsg.Username = message.From.FirstName
|
rmsg.Username = message.From.FirstName
|
||||||
}
|
}
|
||||||
|
if b.GetBool("UseFullName") {
|
||||||
|
if message.From.FirstName != "" && message.From.LastName != "" {
|
||||||
|
rmsg.Username = message.From.FirstName + " " + message.From.LastName
|
||||||
|
}
|
||||||
|
}
|
||||||
if rmsg.Username == "" {
|
if rmsg.Username == "" {
|
||||||
rmsg.Username = message.From.UserName
|
rmsg.Username = message.From.UserName
|
||||||
if rmsg.Username == "" {
|
if rmsg.Username == "" {
|
||||||
@ -110,6 +150,35 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if message.SenderChat != nil { //nolint:nestif
|
||||||
|
rmsg.UserID = strconv.FormatInt(message.SenderChat.ID, 10)
|
||||||
|
if b.GetBool("UseFirstName") {
|
||||||
|
rmsg.Username = message.SenderChat.FirstName
|
||||||
|
}
|
||||||
|
if b.GetBool("UseFullName") {
|
||||||
|
if message.SenderChat.FirstName != "" && message.SenderChat.LastName != "" {
|
||||||
|
rmsg.Username = message.SenderChat.FirstName + " " + message.SenderChat.LastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
|
||||||
|
rmsg.Username = message.SenderChat.UserName
|
||||||
|
|
||||||
|
if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
|
||||||
|
rmsg.Username = message.SenderChat.FirstName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// only download avatars if we have a place to upload them (configured mediaserver)
|
||||||
|
if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") {
|
||||||
|
b.handleDownloadAvatar(message.SenderChat.ID, rmsg.Channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback on author signature (used in "channel" type of chat)
|
||||||
|
if rmsg.Username == "" && message.AuthorSignature != "" {
|
||||||
|
rmsg.Username = message.AuthorSignature
|
||||||
|
}
|
||||||
|
|
||||||
// if we really didn't find a username, set it to unknown
|
// if we really didn't find a username, set it to unknown
|
||||||
if rmsg.Username == "" {
|
if rmsg.Username == "" {
|
||||||
rmsg.Username = unknownUser
|
rmsg.Username = unknownUser
|
||||||
@ -122,10 +191,16 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
|
|
||||||
if update.Message == nil && update.ChannelPost == nil &&
|
if update.Message == nil && update.ChannelPost == nil &&
|
||||||
update.EditedMessage == nil && update.EditedChannelPost == nil {
|
update.EditedMessage == nil && update.EditedChannelPost == nil {
|
||||||
b.Log.Error("Getting nil messages, this shouldn't happen.")
|
b.Log.Info("Received event without messages, skipping.")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.GetInt("debuglevel") == 1 {
|
||||||
|
spew.Dump(update.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.handleGroupUpdate(update)
|
||||||
|
|
||||||
var message *tgbotapi.Message
|
var message *tgbotapi.Message
|
||||||
|
|
||||||
rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
|
rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
|
||||||
@ -144,6 +219,19 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
// set the ID's from the channel or group message
|
// set the ID's from the channel or group message
|
||||||
rmsg.ID = strconv.Itoa(message.MessageID)
|
rmsg.ID = strconv.Itoa(message.MessageID)
|
||||||
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
|
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
|
||||||
|
if message.IsTopicMessage {
|
||||||
|
rmsg.Channel += "/" + strconv.Itoa(message.MessageThreadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// preserve threading from telegram reply
|
||||||
|
if message.ReplyToMessage != nil &&
|
||||||
|
// Used to check if the message was a reply to the root topic
|
||||||
|
(!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) {
|
||||||
|
rmsg.ParentID = strconv.Itoa(message.ReplyToMessage.MessageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle entities (adding URLs)
|
||||||
|
b.handleEntities(&rmsg, message)
|
||||||
|
|
||||||
// handle username
|
// handle username
|
||||||
b.handleUsername(&rmsg, message)
|
b.handleUsername(&rmsg, message)
|
||||||
@ -160,14 +248,12 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
// quote the previous message
|
// quote the previous message
|
||||||
b.handleQuoting(&rmsg, message)
|
b.handleQuoting(&rmsg, message)
|
||||||
|
|
||||||
// handle entities (adding URLs)
|
|
||||||
b.handleEntities(&rmsg, message)
|
|
||||||
|
|
||||||
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
|
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
|
||||||
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
|
// Comment the next line out due to avoid removing empty lines in Telegram
|
||||||
|
// rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
|
||||||
// channels don't have (always?) user information. see #410
|
// channels don't have (always?) user information. see #410
|
||||||
if message.From != nil {
|
if message.From != nil {
|
||||||
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
|
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.FormatInt(message.From.ID, 10), b.General)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
|
||||||
@ -177,61 +263,99 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleDownloadAvatar downloads the avatar of userid from channel
|
func (b *Btelegram) handleGroupUpdate(update tgbotapi.Update) {
|
||||||
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
|
if msg := update.Message; msg != nil {
|
||||||
// logs an error message if it fails
|
switch {
|
||||||
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
|
case msg.NewChatMembers != nil:
|
||||||
rmsg := config.Message{Username: "system",
|
b.handleUserJoin(update)
|
||||||
Text: "avatar",
|
case msg.LeftChatMember != nil:
|
||||||
Channel: channel,
|
b.handleUserLeave(update)
|
||||||
Account: b.Account,
|
|
||||||
UserID: strconv.Itoa(userid),
|
|
||||||
Event: config.EventAvatarDownload,
|
|
||||||
Extra: make(map[string][]interface{})}
|
|
||||||
|
|
||||||
if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
|
|
||||||
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
|
|
||||||
if err != nil {
|
|
||||||
b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(photos.Photos) > 0 {
|
|
||||||
photo := photos.Photos[0][0]
|
|
||||||
url := b.getFileDirectURL(photo.FileID)
|
|
||||||
name := strconv.Itoa(userid) + ".png"
|
|
||||||
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
|
|
||||||
|
|
||||||
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
|
|
||||||
if err != nil {
|
|
||||||
b.Log.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := helper.DownloadFile(url)
|
|
||||||
if err != nil {
|
|
||||||
b.Log.Errorf("download %s failed %#v", url, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
|
|
||||||
b.Remote <- rmsg
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) handleUserJoin(update tgbotapi.Update) {
|
||||||
|
msg := update.Message
|
||||||
|
for _, user := range msg.NewChatMembers {
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: strconv.FormatInt(user.ID, 10),
|
||||||
|
Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me)
|
||||||
|
Channel: strconv.FormatInt(msg.Chat.ID, 10),
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Event: config.EventJoinLeave,
|
||||||
|
Text: "joined chat",
|
||||||
|
}
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) handleUserLeave(update tgbotapi.Update) {
|
||||||
|
msg := update.Message
|
||||||
|
user := msg.LeftChatMember
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: strconv.FormatInt(user.ID, 10),
|
||||||
|
Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me)
|
||||||
|
Channel: strconv.FormatInt(msg.Chat.ID, 10),
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Event: config.EventJoinLeave,
|
||||||
|
Text: "left chat",
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDownloadAvatar downloads the avatar of userid from channel
|
||||||
|
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
|
||||||
|
// logs an error message if it fails
|
||||||
|
func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) {
|
||||||
|
rmsg := config.Message{
|
||||||
|
Username: "system",
|
||||||
|
Text: "avatar",
|
||||||
|
Channel: channel,
|
||||||
|
Account: b.Account,
|
||||||
|
UserID: strconv.FormatInt(userid, 10),
|
||||||
|
Event: config.EventAvatarDownload,
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := b.avatarMap[strconv.FormatInt(userid, 10)]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(photos.Photos) > 0 {
|
||||||
|
photo := photos.Photos[0][0]
|
||||||
|
url := b.getFileDirectURL(photo.FileID)
|
||||||
|
name := strconv.FormatInt(userid, 10) + ".png"
|
||||||
|
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
|
||||||
|
|
||||||
|
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := helper.DownloadFile(url)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("download %s failed %#v", url, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) {
|
func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) {
|
||||||
var format string
|
format := b.GetString("MediaConvertTgs")
|
||||||
switch b.GetString("MediaConvertTgs") {
|
if helper.SupportsFormat(format) {
|
||||||
case FormatWebp:
|
b.Log.Debugf("Format supported by %s, converting %v", helper.LottieBackend(), name)
|
||||||
b.Log.Debugf("Tgs to WebP conversion enabled, converting %v", name)
|
} else {
|
||||||
format = FormatWebp
|
|
||||||
case FormatPng:
|
|
||||||
// The WebP to PNG converter can't handle animated webp files yet,
|
|
||||||
// and I'm not going to write a path for x/image/webp.
|
|
||||||
// The error message would be:
|
|
||||||
// conversion failed: webp: non-Alpha VP8X is not implemented
|
|
||||||
// So instead, we tell lottie to directly go to PNG.
|
|
||||||
b.Log.Debugf("Tgs to PNG conversion enabled, converting %v", name)
|
|
||||||
format = FormatPng
|
|
||||||
default:
|
|
||||||
// Otherwise, no conversion was requested. Trying to run the usual webp
|
// Otherwise, no conversion was requested. Trying to run the usual webp
|
||||||
// converter would fail, because '.tgs.webp' is actually a gzipped JSON
|
// converter would fail, because '.tgs.webp' is actually a gzipped JSON
|
||||||
// file, and has nothing to do with WebP.
|
// file, and has nothing to do with WebP.
|
||||||
@ -259,12 +383,12 @@ func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) {
|
|||||||
|
|
||||||
// handleDownloadFile handles file download
|
// handleDownloadFile handles file download
|
||||||
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
|
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
|
||||||
size := 0
|
size := int64(0)
|
||||||
var url, name, text string
|
var url, name, text string
|
||||||
switch {
|
switch {
|
||||||
case message.Sticker != nil:
|
case message.Sticker != nil:
|
||||||
text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
|
text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
|
||||||
size = message.Sticker.FileSize
|
size = int64(message.Sticker.FileSize)
|
||||||
case message.Voice != nil:
|
case message.Voice != nil:
|
||||||
text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
|
text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
|
||||||
size = message.Voice.FileSize
|
size = message.Voice.FileSize
|
||||||
@ -280,8 +404,8 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
|
|||||||
name = message.Document.FileName
|
name = message.Document.FileName
|
||||||
text = " " + message.Document.FileName + " : " + url
|
text = " " + message.Document.FileName + " : " + url
|
||||||
case message.Photo != nil:
|
case message.Photo != nil:
|
||||||
photos := *message.Photo
|
photos := message.Photo
|
||||||
size = photos[len(photos)-1].FileSize
|
size = int64(photos[len(photos)-1].FileSize)
|
||||||
text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
|
text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,6 +435,11 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
|
|||||||
b.maybeConvertWebp(&name, data)
|
b.maybeConvertWebp(&name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rename .oga to .ogg https://github.com/42wim/matterbridge/issues/906#issuecomment-741793512
|
||||||
|
if strings.HasSuffix(name, ".oga") && message.Audio != nil {
|
||||||
|
name = strings.Replace(name, ".oga", ".ogg", 1)
|
||||||
|
}
|
||||||
|
|
||||||
helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
|
helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -322,7 +451,7 @@ func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (str
|
|||||||
urlPart := strings.Split(url, "/")
|
urlPart := strings.Split(url, "/")
|
||||||
name = urlPart[len(urlPart)-1]
|
name = urlPart[len(urlPart)-1]
|
||||||
}
|
}
|
||||||
if suffix != "" && !strings.HasSuffix(name, suffix) {
|
if suffix != "" && !strings.HasSuffix(name, suffix) && !strings.HasSuffix(name, ".webm") {
|
||||||
name += suffix
|
name += suffix
|
||||||
}
|
}
|
||||||
text := " " + url
|
text := " " + url
|
||||||
@ -334,11 +463,15 @@ func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, err
|
|||||||
if msg.ID == "" {
|
if msg.ID == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
msgid, err := strconv.Atoi(msg.ID)
|
msgid, err := strconv.Atoi(msg.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
|
|
||||||
|
cfg := tgbotapi.NewDeleteMessage(chatid, msgid)
|
||||||
|
_, err = b.c.Request(cfg)
|
||||||
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,31 +509,57 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleUploadFile handles native upload of files
|
// handleUploadFile handles native upload of files
|
||||||
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
|
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64, threadid int, parentID int) (string, error) {
|
||||||
var c tgbotapi.Chattable
|
var media []interface{}
|
||||||
for _, f := range msg.Extra["file"] {
|
for _, f := range msg.Extra["file"] {
|
||||||
fi := f.(config.FileInfo)
|
fi := f.(config.FileInfo)
|
||||||
file := tgbotapi.FileBytes{
|
file := tgbotapi.FileBytes{
|
||||||
Name: fi.Name,
|
Name: fi.Name,
|
||||||
Bytes: *fi.Data,
|
Bytes: *fi.Data,
|
||||||
}
|
}
|
||||||
re := regexp.MustCompile(".(jpg|png)$")
|
|
||||||
if re.MatchString(fi.Name) {
|
if b.GetString("MessageFormat") == HTMLFormat {
|
||||||
c = tgbotapi.NewPhotoUpload(chatid, file)
|
fi.Comment = makeHTML(html.EscapeString(fi.Comment))
|
||||||
} else {
|
|
||||||
c = tgbotapi.NewDocumentUpload(chatid, file)
|
|
||||||
}
|
}
|
||||||
_, err := b.c.Send(c)
|
|
||||||
if err != nil {
|
switch filepath.Ext(fi.Name) {
|
||||||
b.Log.Errorf("file upload failed: %#v", err)
|
case ".jpg", ".jpe", ".png":
|
||||||
}
|
pc := tgbotapi.NewInputMediaPhoto(file)
|
||||||
if fi.Comment != "" {
|
if fi.Comment != "" {
|
||||||
if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil {
|
pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
|
||||||
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 ""
|
|
||||||
|
return b.sendMediaFiles(msg, chatid, threadid, parentID, media)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
|
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
|
||||||
@ -408,7 +567,7 @@ func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string
|
|||||||
if format == "" {
|
if format == "" {
|
||||||
format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
|
format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
|
||||||
}
|
}
|
||||||
quoteMessagelength := len(quoteMessage)
|
quoteMessagelength := len([]rune(quoteMessage))
|
||||||
if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") {
|
if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") {
|
||||||
runes := []rune(quoteMessage)
|
runes := []rune(quoteMessage)
|
||||||
quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")])
|
quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")])
|
||||||
@ -427,21 +586,61 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa
|
|||||||
if message.Entities == nil {
|
if message.Entities == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// for now only do URL replacements
|
|
||||||
for _, e := range *message.Entities {
|
indexMovedBy := 0
|
||||||
|
prevLinkOffset := -1
|
||||||
|
|
||||||
|
for _, e := range message.Entities {
|
||||||
|
|
||||||
|
asRunes := utf16.Encode([]rune(rmsg.Text))
|
||||||
|
|
||||||
if e.Type == "text_link" {
|
if e.Type == "text_link" {
|
||||||
|
offset := e.Offset + indexMovedBy
|
||||||
url, err := e.ParseURL()
|
url, err := e.ParseURL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("entity text_link url parse failed: %s", err)
|
b.Log.Errorf("entity text_link url parse failed: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
utfEncodedString := utf16.Encode([]rune(rmsg.Text))
|
utfEncodedString := utf16.Encode([]rune(rmsg.Text))
|
||||||
if e.Offset+e.Length > len(utfEncodedString) {
|
if offset+e.Length > len(utfEncodedString) {
|
||||||
b.Log.Errorf("entity length is too long %d > %d", e.Offset+e.Length, len(utfEncodedString))
|
b.Log.Errorf("entity length is too long %d > %d", offset+e.Length, len(utfEncodedString))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length])
|
rmsg.Text = string(utf16.Decode(asRunes[:offset+e.Length])) + " (" + url.String() + ")" + string(utf16.Decode(asRunes[offset+e.Length:]))
|
||||||
rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1)
|
indexMovedBy += len(url.String()) + 3
|
||||||
|
prevLinkOffset = e.Offset
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Offset == prevLinkOffset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Type == "code" {
|
||||||
|
offset := e.Offset + indexMovedBy
|
||||||
|
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "`" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "`" + string(utf16.Decode(asRunes[offset+e.Length:]))
|
||||||
|
indexMovedBy += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Type == "pre" {
|
||||||
|
offset := e.Offset + indexMovedBy
|
||||||
|
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "```\n" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "```\n" + string(utf16.Decode(asRunes[offset+e.Length:]))
|
||||||
|
indexMovedBy += 8
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Type == "bold" {
|
||||||
|
offset := e.Offset + indexMovedBy
|
||||||
|
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "*" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "*" + string(utf16.Decode(asRunes[offset+e.Length:]))
|
||||||
|
indexMovedBy += 2
|
||||||
|
}
|
||||||
|
if e.Type == "italic" {
|
||||||
|
offset := e.Offset + indexMovedBy
|
||||||
|
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "_" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "_" + string(utf16.Decode(asRunes[offset+e.Length:]))
|
||||||
|
indexMovedBy += 2
|
||||||
|
}
|
||||||
|
if e.Type == "strike" {
|
||||||
|
offset := e.Offset + indexMovedBy
|
||||||
|
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "~" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "~" + string(utf16.Decode(asRunes[offset+e.Length:]))
|
||||||
|
indexMovedBy += 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package btelegram
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"html"
|
|
||||||
|
|
||||||
"github.com/russross/blackfriday"
|
"github.com/russross/blackfriday"
|
||||||
)
|
)
|
||||||
@ -24,10 +23,16 @@ func (options *customHTML) Paragraph(out *bytes.Buffer, text func() bool) {
|
|||||||
func (options *customHTML) BlockCode(out *bytes.Buffer, text []byte, lang string) {
|
func (options *customHTML) BlockCode(out *bytes.Buffer, text []byte, lang string) {
|
||||||
out.WriteString("<pre>")
|
out.WriteString("<pre>")
|
||||||
|
|
||||||
out.WriteString(html.EscapeString(string(text)))
|
out.WriteString(string(text))
|
||||||
out.WriteString("</pre>\n")
|
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) {
|
func (options *customHTML) Header(out *bytes.Buffer, text func() bool, level int, id string) {
|
||||||
options.Paragraph(out, text)
|
options.Paragraph(out, text)
|
||||||
}
|
}
|
||||||
@ -42,6 +47,10 @@ func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) {
|
|||||||
out.WriteByte('\n')
|
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) {
|
func (options *customHTML) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||||
options.Paragraph(out, text)
|
options.Paragraph(out, text)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package btelegram
|
package btelegram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -9,7 +10,7 @@ import (
|
|||||||
"github.com/42wim/matterbridge/bridge"
|
"github.com/42wim/matterbridge/bridge"
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -17,8 +18,6 @@ const (
|
|||||||
HTMLFormat = "HTML"
|
HTMLFormat = "HTML"
|
||||||
HTMLNick = "htmlnick"
|
HTMLNick = "htmlnick"
|
||||||
MarkdownV2 = "MarkdownV2"
|
MarkdownV2 = "MarkdownV2"
|
||||||
FormatPng = "png"
|
|
||||||
FormatWebp = "webp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Btelegram struct {
|
type Btelegram struct {
|
||||||
@ -32,10 +31,10 @@ func New(cfg *bridge.Config) bridge.Bridger {
|
|||||||
if tgsConvertFormat != "" {
|
if tgsConvertFormat != "" {
|
||||||
err := helper.CanConvertTgsToX()
|
err := helper.CanConvertTgsToX()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but lottie does not appear to work:\n%#v", tgsConvertFormat, err)
|
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s does not appear to work:\n%#v", tgsConvertFormat, helper.LottieBackend(), err)
|
||||||
}
|
}
|
||||||
if tgsConvertFormat != FormatPng && tgsConvertFormat != FormatWebp {
|
if !helper.SupportsFormat(tgsConvertFormat) {
|
||||||
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but only '%s' and '%s' are supported.", FormatPng, FormatWebp, tgsConvertFormat)
|
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s doesn't support it.", tgsConvertFormat, helper.LottieBackend())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &Btelegram{Config: cfg, avatarMap: make(map[string]string)}
|
return &Btelegram{Config: cfg, avatarMap: make(map[string]string)}
|
||||||
@ -51,11 +50,7 @@ func (b *Btelegram) Connect() error {
|
|||||||
}
|
}
|
||||||
u := tgbotapi.NewUpdate(0)
|
u := tgbotapi.NewUpdate(0)
|
||||||
u.Timeout = 60
|
u.Timeout = 60
|
||||||
updates, err := b.c.GetUpdatesChan(u)
|
updates := b.c.GetUpdatesChan(u)
|
||||||
if err != nil {
|
|
||||||
b.Log.Debugf("%#v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.Log.Info("Connection succeeded")
|
b.Log.Info("Connection succeeded")
|
||||||
go b.handleRecv(updates)
|
go b.handleRecv(updates)
|
||||||
return nil
|
return nil
|
||||||
@ -69,11 +64,63 @@ func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TGGetParseMode(b *Btelegram, username string, text string) (textout string, parsemode string) {
|
||||||
|
textout = username + text
|
||||||
|
if b.GetString("MessageFormat") == HTMLFormat {
|
||||||
|
b.Log.Debug("Using mode HTML")
|
||||||
|
parsemode = tgbotapi.ModeHTML
|
||||||
|
}
|
||||||
|
if b.GetString("MessageFormat") == "Markdown" {
|
||||||
|
b.Log.Debug("Using mode markdown")
|
||||||
|
parsemode = tgbotapi.ModeMarkdown
|
||||||
|
}
|
||||||
|
if b.GetString("MessageFormat") == MarkdownV2 {
|
||||||
|
b.Log.Debug("Using mode MarkdownV2")
|
||||||
|
parsemode = MarkdownV2
|
||||||
|
}
|
||||||
|
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
|
||||||
|
b.Log.Debug("Using mode HTML - nick only")
|
||||||
|
textout = username + html.EscapeString(text)
|
||||||
|
parsemode = tgbotapi.ModeHTML
|
||||||
|
}
|
||||||
|
return textout, parsemode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) getIds(channel string) (int64, int, error) {
|
||||||
|
var chatid int64
|
||||||
|
topicid := 0
|
||||||
|
|
||||||
|
// get the chatid
|
||||||
|
if strings.Contains(channel, "/") { //nolint:nestif
|
||||||
|
s := strings.Split(channel, "/")
|
||||||
|
if len(s) < 2 {
|
||||||
|
b.Log.Errorf("Invalid channel format: %#v\n", channel)
|
||||||
|
return 0, 0, nil
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseInt(s[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
chatid = id
|
||||||
|
tid, err := strconv.Atoi(s[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
topicid = tid
|
||||||
|
} else {
|
||||||
|
id, err := strconv.ParseInt(channel, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
chatid = id
|
||||||
|
}
|
||||||
|
return chatid, topicid, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Btelegram) Send(msg config.Message) (string, error) {
|
func (b *Btelegram) Send(msg config.Message) (string, error) {
|
||||||
b.Log.Debugf("=> Receiving %#v", msg)
|
b.Log.Debugf("=> Receiving %#v", msg)
|
||||||
|
|
||||||
// get the chatid
|
chatid, topicid, err := b.getIds(msg.Channel)
|
||||||
chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -84,7 +131,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b.GetString("MessageFormat") == HTMLFormat {
|
if b.GetString("MessageFormat") == HTMLFormat {
|
||||||
msg.Text = makeHTML(msg.Text)
|
msg.Text = makeHTML(html.EscapeString(msg.Text))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete message
|
// Delete message
|
||||||
@ -92,16 +139,27 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
|
|||||||
return b.handleDelete(&msg, chatid)
|
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
|
// Upload a file if it exists
|
||||||
if msg.Extra != nil {
|
if msg.Extra != nil {
|
||||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||||
if _, msgErr := b.sendMessage(chatid, rmsg.Username, rmsg.Text); msgErr != nil {
|
if _, msgErr := b.sendMessage(chatid, topicid, rmsg.Username, rmsg.Text, parentID); msgErr != nil {
|
||||||
b.Log.Errorf("sendMessage failed: %s", msgErr)
|
b.Log.Errorf("sendMessage failed: %s", msgErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check if we have files to upload (from slack, telegram or mattermost)
|
// check if we have files to upload (from slack, telegram or mattermost)
|
||||||
if len(msg.Extra["file"]) > 0 {
|
if len(msg.Extra["file"]) > 0 {
|
||||||
b.handleUploadFile(&msg, chatid)
|
return b.handleUploadFile(&msg, chatid, topicid, parentID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +173,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
|
|||||||
// Ignore empty text field needs for prevent double messages from whatsapp to telegram
|
// Ignore empty text field needs for prevent double messages from whatsapp to telegram
|
||||||
// when sending media with text caption
|
// when sending media with text caption
|
||||||
if msg.Text != "" {
|
if msg.Text != "" {
|
||||||
return b.sendMessage(chatid, msg.Username, msg.Text)
|
return b.sendMessage(chatid, topicid, msg.Username, msg.Text, parentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil
|
return "", nil
|
||||||
@ -129,27 +187,13 @@ func (b *Btelegram) getFileDirectURL(id string) string {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) {
|
func (b *Btelegram) sendMessage(chatid int64, topicid int, username, text string, parentID int) (string, error) {
|
||||||
m := tgbotapi.NewMessage(chatid, "")
|
m := tgbotapi.NewMessage(chatid, "")
|
||||||
m.Text = username + text
|
m.Text, m.ParseMode = TGGetParseMode(b, username, text)
|
||||||
if b.GetString("MessageFormat") == HTMLFormat {
|
if topicid != 0 {
|
||||||
b.Log.Debug("Using mode HTML")
|
m.BaseChat.MessageThreadID = topicid
|
||||||
m.ParseMode = tgbotapi.ModeHTML
|
|
||||||
}
|
}
|
||||||
if b.GetString("MessageFormat") == "Markdown" {
|
m.ReplyToMessageID = parentID
|
||||||
b.Log.Debug("Using mode markdown")
|
|
||||||
m.ParseMode = tgbotapi.ModeMarkdown
|
|
||||||
}
|
|
||||||
if b.GetString("MessageFormat") == MarkdownV2 {
|
|
||||||
b.Log.Debug("Using mode MarkdownV2")
|
|
||||||
m.ParseMode = MarkdownV2
|
|
||||||
}
|
|
||||||
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
|
|
||||||
b.Log.Debug("Using mode HTML - nick only")
|
|
||||||
m.Text = username + html.EscapeString(text)
|
|
||||||
m.ParseMode = tgbotapi.ModeHTML
|
|
||||||
}
|
|
||||||
|
|
||||||
m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
|
m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
|
||||||
|
|
||||||
res, err := b.c.Send(m)
|
res, err := b.c.Send(m)
|
||||||
@ -159,6 +203,37 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, er
|
|||||||
return strconv.Itoa(res.MessageID), nil
|
return strconv.Itoa(res.MessageID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendMediaFiles native upload media files via media group
|
||||||
|
func (b *Btelegram) sendMediaFiles(msg *config.Message, chatid int64, threadid int, parentID int, media []interface{}) (string, error) {
|
||||||
|
if len(media) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
mg := tgbotapi.MediaGroupConfig{
|
||||||
|
BaseChat: tgbotapi.BaseChat{
|
||||||
|
ChatID: chatid,
|
||||||
|
MessageThreadID: threadid,
|
||||||
|
ChannelUsername: msg.Username,
|
||||||
|
ReplyToMessageID: parentID,
|
||||||
|
},
|
||||||
|
Media: media,
|
||||||
|
}
|
||||||
|
messages, err := b.c.SendMediaGroup(mg)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// return first message id
|
||||||
|
return strconv.Itoa(messages[0].MessageID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// intParentID return integer parent id for telegram message
|
||||||
|
func (b *Btelegram) intParentID(parentID string) (int, error) {
|
||||||
|
pid, err := strconv.Atoi(parentID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) {
|
func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) {
|
||||||
fi := msg.Extra["file"][0].(config.FileInfo)
|
fi := msg.Extra["file"][0].(config.FileInfo)
|
||||||
/* if we have a sha we have successfully uploaded the file to the media server,
|
/* if we have a sha we have successfully uploaded the file to the media server,
|
||||||
|
333
bridge/vk/vk.go
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
package bvk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/42wim/matterbridge/bridge"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
|
|
||||||
|
"github.com/SevereCloud/vksdk/v2/api"
|
||||||
|
"github.com/SevereCloud/vksdk/v2/events"
|
||||||
|
longpoll "github.com/SevereCloud/vksdk/v2/longpoll-bot"
|
||||||
|
"github.com/SevereCloud/vksdk/v2/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
audioMessage = "audio_message"
|
||||||
|
document = "doc"
|
||||||
|
photo = "photo"
|
||||||
|
video = "video"
|
||||||
|
graffiti = "graffiti"
|
||||||
|
sticker = "sticker"
|
||||||
|
wall = "wall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type user struct {
|
||||||
|
lastname, firstname, avatar string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bvk struct {
|
||||||
|
c *api.VK
|
||||||
|
lp *longpoll.LongPoll
|
||||||
|
usernamesMap map[int]user // cache of user names and avatar URLs
|
||||||
|
*bridge.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg *bridge.Config) bridge.Bridger {
|
||||||
|
return &Bvk{usernamesMap: make(map[int]user), Config: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bvk) Connect() error {
|
||||||
|
b.Log.Info("Connecting")
|
||||||
|
b.c = api.NewVK(b.GetString("Token"))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
b.lp, err = longpoll.NewLongPollCommunity(b.c)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Debugf("%#v", err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) {
|
||||||
|
b.handleMessage(obj.Message, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Log.Info("Connection succeeded")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := b.lp.Run()
|
||||||
|
if err != nil {
|
||||||
|
b.Log.WithError(err).Fatal("Enable longpoll in group management")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bvk) Disconnect() error {
|
||||||
|
b.lp.Shutdown()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bvk) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bvk) Send(msg config.Message) (string, error) {
|
||||||
|
b.Log.Debugf("=> Receiving %#v", msg)
|
||||||
|
|
||||||
|
peerID, err := strconv.Atoi(msg.Channel)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
params := api.Params{}
|
||||||
|
|
||||||
|
text := msg.Username + msg.Text
|
||||||
|
|
||||||
|
if msg.Extra != nil {
|
||||||
|
if len(msg.Extra["file"]) > 0 {
|
||||||
|
// generate attachments string
|
||||||
|
attachment, urls := b.uploadFiles(msg.Extra, peerID)
|
||||||
|
params["attachment"] = attachment
|
||||||
|
text += urls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params["message"] = text
|
||||||
|
|
||||||
|
if msg.ID == "" {
|
||||||
|
// New message
|
||||||
|
params["random_id"] = time.Now().Unix()
|
||||||
|
params["peer_ids"] = msg.Channel
|
||||||
|
|
||||||
|
res, e := b.c.MessagesSendPeerIDs(params)
|
||||||
|
if e != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Itoa(res[0].ConversationMessageID), nil
|
||||||
|
}
|
||||||
|
// Edit message
|
||||||
|
messageID, err := strconv.ParseInt(msg.ID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
params["peer_id"] = peerID
|
||||||
|
params["conversation_message_id"] = messageID
|
||||||
|
|
||||||
|
_, err = b.c.MessagesEdit(params)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bvk) getUser(id int) user {
|
||||||
|
u, found := b.usernamesMap[id]
|
||||||
|
if !found {
|
||||||
|
b.Log.Debug("Fetching username for ", id)
|
||||||
|
|
||||||
|
if id >= 0 {
|
||||||
|
result, _ := b.c.UsersGet(api.Params{
|
||||||
|
"user_ids": id,
|
||||||
|
"fields": "photo_200",
|
||||||
|
})
|
||||||
|
|
||||||
|
resUser := result[0]
|
||||||
|
u = user{lastname: resUser.LastName, firstname: resUser.FirstName, avatar: resUser.Photo200}
|
||||||
|
b.usernamesMap[id] = u
|
||||||
|
} else {
|
||||||
|
result, _ := b.c.GroupsGetByID(api.Params{
|
||||||
|
"group_id": id * -1,
|
||||||
|
})
|
||||||
|
|
||||||
|
resGroup := result[0]
|
||||||
|
u = user{lastname: resGroup.Name, avatar: resGroup.Photo200}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bvk) handleMessage(msg object.MessagesMessage, isFwd bool) {
|
||||||
|
b.Log.Debug("ChatID: ", msg.PeerID)
|
||||||
|
// fetch user info
|
||||||
|
u := b.getUser(msg.FromID)
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
Text: msg.Text,
|
||||||
|
Username: u.firstname + " " + u.lastname,
|
||||||
|
Avatar: u.avatar,
|
||||||
|
Channel: strconv.Itoa(msg.PeerID),
|
||||||
|
Account: b.Account,
|
||||||
|
UserID: strconv.Itoa(msg.FromID),
|
||||||
|
ID: strconv.Itoa(msg.ConversationMessageID),
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.ReplyMessage != nil {
|
||||||
|
ur := b.getUser(msg.ReplyMessage.FromID)
|
||||||
|
rmsg.Text = "Re: " + ur.firstname + " " + ur.lastname + "\n" + rmsg.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFwd {
|
||||||
|
rmsg.Username = "Fwd: " + rmsg.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msg.Attachments) > 0 {
|
||||||
|
urls, text := b.getFiles(msg.Attachments)
|
||||||
|
|
||||||
|
if text != "" {
|
||||||
|
rmsg.Text += "\n" + text
|
||||||
|
}
|
||||||
|
|
||||||
|
// download
|
||||||
|
b.downloadFiles(&rmsg, urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msg.FwdMessages) > 0 {
|
||||||
|
rmsg.Text += strconv.Itoa(len(msg.FwdMessages)) + " forwarded messages"
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
|
||||||
|
if len(msg.FwdMessages) > 0 {
|
||||||
|
// recursive processing of forwarded messages
|
||||||
|
for _, m := range msg.FwdMessages {
|
||||||
|
m.PeerID = msg.PeerID
|
||||||
|
b.handleMessage(m, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, string) {
|
||||||
|
var attachments []string
|
||||||
|
text := ""
|
||||||
|
|
||||||
|
for _, f := range extra["file"] {
|
||||||
|
fi := f.(config.FileInfo)
|
||||||
|
|
||||||
|
if fi.Comment != "" {
|
||||||
|
text += fi.Comment + "\n"
|
||||||
|
}
|
||||||
|
a, err := b.uploadFile(fi, peerID)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.WithError(err).Error("File upload error ", fi.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments = append(attachments, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(attachments, ","), text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) {
|
||||||
|
r := bytes.NewReader(*file.Data)
|
||||||
|
|
||||||
|
photoRE := regexp.MustCompile(".(jpg|jpe|png)$")
|
||||||
|
if photoRE.MatchString(file.Name) {
|
||||||
|
// BUG(VK): for community chat peerID=0
|
||||||
|
p, err := b.c.UploadMessagesPhoto(0, r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return photo + strconv.Itoa(p[0].OwnerID) + "_" + strconv.Itoa(p[0].ID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var doctype string
|
||||||
|
if strings.Contains(file.Name, ".ogg") {
|
||||||
|
doctype = audioMessage
|
||||||
|
} else {
|
||||||
|
doctype = document
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := b.c.UploadMessagesDoc(peerID, doctype, file.Name, "", r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch doc.Type {
|
||||||
|
case audioMessage:
|
||||||
|
return document + strconv.Itoa(doc.AudioMessage.OwnerID) + "_" + strconv.Itoa(doc.AudioMessage.ID), nil
|
||||||
|
case document:
|
||||||
|
return document + strconv.Itoa(doc.Doc.OwnerID) + "_" + strconv.Itoa(doc.Doc.ID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bvk) getFiles(attachments []object.MessagesMessageAttachment) ([]string, string) {
|
||||||
|
var urls []string
|
||||||
|
var text []string
|
||||||
|
|
||||||
|
for _, a := range attachments {
|
||||||
|
switch a.Type {
|
||||||
|
case photo:
|
||||||
|
var resolution float64 = 0
|
||||||
|
url := a.Photo.Sizes[0].URL
|
||||||
|
for _, size := range a.Photo.Sizes {
|
||||||
|
r := size.Height * size.Width
|
||||||
|
if resolution < r {
|
||||||
|
resolution = r
|
||||||
|
url = size.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urls = append(urls, url)
|
||||||
|
|
||||||
|
case document:
|
||||||
|
urls = append(urls, a.Doc.URL)
|
||||||
|
|
||||||
|
case graffiti:
|
||||||
|
urls = append(urls, a.Graffiti.URL)
|
||||||
|
|
||||||
|
case audioMessage:
|
||||||
|
urls = append(urls, a.AudioMessage.DocsDocPreviewAudioMessage.LinkOgg)
|
||||||
|
|
||||||
|
case sticker:
|
||||||
|
var resolution float64 = 0
|
||||||
|
url := a.Sticker.Images[0].URL
|
||||||
|
for _, size := range a.Sticker.Images {
|
||||||
|
r := size.Height * size.Width
|
||||||
|
if resolution < r {
|
||||||
|
resolution = r
|
||||||
|
url = size.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
urls = append(urls, url+".png")
|
||||||
|
case video:
|
||||||
|
text = append(text, "https://vk.com/video"+strconv.Itoa(a.Video.OwnerID)+"_"+strconv.Itoa(a.Video.ID))
|
||||||
|
|
||||||
|
case wall:
|
||||||
|
text = append(text, "https://vk.com/wall"+strconv.Itoa(a.Wall.FromID)+"_"+strconv.Itoa(a.Wall.ID))
|
||||||
|
|
||||||
|
default:
|
||||||
|
text = append(text, "This attachment is not supported ("+a.Type+")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return urls, strings.Join(text, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bvk) downloadFiles(rmsg *config.Message, urls []string) {
|
||||||
|
for _, url := range urls {
|
||||||
|
data, err := helper.DownloadFile(url)
|
||||||
|
if err == nil {
|
||||||
|
urlPart := strings.Split(url, "/")
|
||||||
|
name := strings.Split(urlPart[len(urlPart)-1], "?")[0]
|
||||||
|
helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
// nolint:goconst
|
||||||
package bwhatsapp
|
package bwhatsapp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -24,7 +25,8 @@ Check:
|
|||||||
func (b *Bwhatsapp) HandleError(err error) {
|
func (b *Bwhatsapp) HandleError(err error) {
|
||||||
// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
|
// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
|
||||||
// ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094
|
// 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") {
|
if strings.Contains(err.Error(), "error processing data: received invalid data") ||
|
||||||
|
strings.Contains(err.Error(), "invalid string with tag 174") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,16 +49,22 @@ func (b *Bwhatsapp) reconnect(err error) {
|
|||||||
Max: 5 * time.Minute,
|
Max: 5 * time.Minute,
|
||||||
Jitter: true,
|
Jitter: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
d := bf.Duration()
|
d := bf.Duration()
|
||||||
|
|
||||||
b.Log.Errorf("Connection failed, underlying error: %v", err)
|
b.Log.Errorf("Connection failed, underlying error: %v", err)
|
||||||
b.Log.Infof("Waiting %s...", d)
|
b.Log.Infof("Waiting %s...", d)
|
||||||
|
|
||||||
time.Sleep(d)
|
time.Sleep(d)
|
||||||
|
|
||||||
b.Log.Info("Reconnecting...")
|
b.Log.Info("Reconnecting...")
|
||||||
|
|
||||||
err := b.conn.Restore()
|
err := b.conn.Restore()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
bf.Reset()
|
bf.Reset()
|
||||||
b.startedAt = uint64(time.Now().Unix())
|
b.startedAt = uint64(time.Now().Unix())
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,7 +72,7 @@ func (b *Bwhatsapp) reconnect(err error) {
|
|||||||
|
|
||||||
// HandleTextMessage sent from WhatsApp, relay it to the brige
|
// HandleTextMessage sent from WhatsApp, relay it to the brige
|
||||||
func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
|
func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
|
||||||
if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") {
|
if message.Info.FromMe {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// whatsapp sends last messages to show context , cut them
|
// whatsapp sends last messages to show context , cut them
|
||||||
@ -72,12 +80,10 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
|
|
||||||
groupJID := message.Info.RemoteJid
|
groupJID := message.Info.RemoteJid
|
||||||
|
|
||||||
senderJID := message.Info.SenderJid
|
senderJID := message.Info.SenderJid
|
||||||
|
|
||||||
if len(senderJID) == 0 {
|
if len(senderJID) == 0 {
|
||||||
// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
|
|
||||||
if message.Info.Source != nil && message.Info.Source.Participant != nil {
|
if message.Info.Source != nil && message.Info.Source.Participant != nil {
|
||||||
senderJID = *message.Info.Source.Participant
|
senderJID = *message.Info.Source.Participant
|
||||||
}
|
}
|
||||||
@ -101,110 +107,276 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
|
|||||||
if mention == "" {
|
if mention == "" {
|
||||||
mention = "someone"
|
mention = "someone"
|
||||||
}
|
}
|
||||||
|
|
||||||
message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
|
message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
|
||||||
rmsg := config.Message{
|
rmsg := config.Message{
|
||||||
UserID: senderJID,
|
UserID: senderJID,
|
||||||
Username: senderName,
|
Username: senderName,
|
||||||
Text: message.Text,
|
Text: message.Text,
|
||||||
Timestamp: messageTime,
|
Channel: groupJID,
|
||||||
Channel: groupJID,
|
Account: b.Account,
|
||||||
Account: b.Account,
|
Protocol: b.Protocol,
|
||||||
Protocol: b.Protocol,
|
Extra: make(map[string][]interface{}),
|
||||||
Extra: make(map[string][]interface{}),
|
|
||||||
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
|
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
|
||||||
// Event string `json:"event"`
|
ID: message.Info.Id,
|
||||||
// Gateway string // will be added during message processing
|
}
|
||||||
ID: message.Info.Id}
|
|
||||||
|
|
||||||
if avatarURL, exists := b.userAvatars[senderJID]; exists {
|
if avatarURL, exists := b.userAvatars[senderJID]; exists {
|
||||||
rmsg.Avatar = avatarURL
|
rmsg.Avatar = avatarURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
||||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|
||||||
b.Remote <- rmsg
|
b.Remote <- rmsg
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleImageMessage sent from WhatsApp, relay it to the brige
|
// HandleImageMessage sent from WhatsApp, relay it to the brige
|
||||||
|
// nolint:funlen
|
||||||
func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
|
func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
|
||||||
if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") {
|
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// whatsapp sends last messages to show context , cut them
|
|
||||||
if message.Info.Timestamp < b.startedAt {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
|
|
||||||
groupJID := message.Info.RemoteJid
|
|
||||||
|
|
||||||
senderJID := message.Info.SenderJid
|
senderJID := message.Info.SenderJid
|
||||||
if len(senderJID) == 0 {
|
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
|
||||||
// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
|
senderJID = *message.Info.Source.Participant
|
||||||
if message.Info.Source != nil && message.Info.Source.Participant != nil {
|
|
||||||
senderJID = *message.Info.Source.Participant
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// translate sender's Jid to the nicest username we can get
|
senderName := b.getSenderName(message.Info.SenderJid)
|
||||||
senderName := b.getSenderName(senderJID)
|
|
||||||
if senderName == "" {
|
if senderName == "" {
|
||||||
senderName = "Someone" // don't expose telephone number
|
senderName = "Someone" // don't expose telephone number
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
|
||||||
rmsg := config.Message{
|
rmsg := config.Message{
|
||||||
UserID: senderJID,
|
UserID: senderJID,
|
||||||
Username: senderName,
|
Username: senderName,
|
||||||
Timestamp: messageTime,
|
Channel: message.Info.RemoteJid,
|
||||||
Channel: groupJID,
|
Account: b.Account,
|
||||||
Account: b.Account,
|
Protocol: b.Protocol,
|
||||||
Protocol: b.Protocol,
|
Extra: make(map[string][]interface{}),
|
||||||
Extra: make(map[string][]interface{}),
|
ID: message.Info.Id,
|
||||||
// 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
|
rmsg.Avatar = avatarURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download and unencrypt content
|
fileExt, err := mime.ExtensionsByType(message.Type)
|
||||||
data, err := message.Download()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Errorf("%v", err)
|
b.Log.Errorf("Mimetype detection error: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file extension by mimetype
|
// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
|
||||||
fileExt, err := mime.ExtensionsByType(message.Type)
|
if fileExt[0] == ".jfif" {
|
||||||
if err != nil {
|
fileExt[0] = ".jpg"
|
||||||
b.Log.Errorf("%v", err)
|
}
|
||||||
return
|
|
||||||
|
// 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])
|
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
|
||||||
|
|
||||||
b.Log.Debugf("<= Image downloaded and unencrypted")
|
b.Log.Debugf("Trying to download %s with type %s", filename, message.Type)
|
||||||
|
|
||||||
|
data, err := message.Download()
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Download image failed: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Move file to bridge storage
|
// Move file to bridge storage
|
||||||
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
|
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
|
||||||
|
|
||||||
b.Log.Debugf("<= Image Message is %#v", rmsg)
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
||||||
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|
||||||
b.Remote <- rmsg
|
b.Remote <- rmsg
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
|
// HandleVideoMessage downloads video messages
|
||||||
// fmt.Println(message) // TODO implement
|
func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
|
||||||
//}
|
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
|
||||||
//
|
return
|
||||||
//func (b *Bwhatsapp) HandleJsonMessage(message string) {
|
}
|
||||||
// fmt.Println(message) // TODO implement
|
|
||||||
//}
|
senderJID := message.Info.SenderJid
|
||||||
// TODO HandleRawMessage
|
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
|
||||||
// TODO HandleAudioMessage
|
senderJID = *message.Info.Source.Participant
|
||||||
|
}
|
||||||
|
|
||||||
|
senderName := b.getSenderName(message.Info.SenderJid)
|
||||||
|
if senderName == "" {
|
||||||
|
senderName = "Someone" // don't expose telephone number
|
||||||
|
}
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: senderJID,
|
||||||
|
Username: senderName,
|
||||||
|
Channel: message.Info.RemoteJid,
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
|
ID: message.Info.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatarURL, exists := b.userAvatars[senderJID]; exists {
|
||||||
|
rmsg.Avatar = avatarURL
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExt, err := mime.ExtensionsByType(message.Type)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Mimetype detection error: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fileExt) == 0 {
|
||||||
|
fileExt = append(fileExt, ".mp4")
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
|
||||||
|
|
||||||
|
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
|
||||||
|
|
||||||
|
data, err := message.Download()
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Download video failed: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move file to bridge storage
|
||||||
|
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
|
||||||
|
|
||||||
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
||||||
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleAudioMessage downloads audio messages
|
||||||
|
func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) {
|
||||||
|
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
senderJID := message.Info.SenderJid
|
||||||
|
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
|
||||||
|
senderJID = *message.Info.Source.Participant
|
||||||
|
}
|
||||||
|
|
||||||
|
senderName := b.getSenderName(message.Info.SenderJid)
|
||||||
|
if senderName == "" {
|
||||||
|
senderName = "Someone" // don't expose telephone number
|
||||||
|
}
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: senderJID,
|
||||||
|
Username: senderName,
|
||||||
|
Channel: message.Info.RemoteJid,
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
|
ID: message.Info.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatarURL, exists := b.userAvatars[senderJID]; exists {
|
||||||
|
rmsg.Avatar = avatarURL
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExt, err := mime.ExtensionsByType(message.Type)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Mimetype detection error: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fileExt) == 0 {
|
||||||
|
fileExt = append(fileExt, ".ogg")
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
|
||||||
|
|
||||||
|
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
|
||||||
|
|
||||||
|
data, err := message.Download()
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Download audio failed: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move file to bridge storage
|
||||||
|
helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
|
||||||
|
|
||||||
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
||||||
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleDocumentMessage downloads documents
|
||||||
|
func (b *Bwhatsapp) HandleDocumentMessage(message whatsapp.DocumentMessage) {
|
||||||
|
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
senderJID := message.Info.SenderJid
|
||||||
|
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
|
||||||
|
senderJID = *message.Info.Source.Participant
|
||||||
|
}
|
||||||
|
|
||||||
|
senderName := b.getSenderName(message.Info.SenderJid)
|
||||||
|
if senderName == "" {
|
||||||
|
senderName = "Someone" // don't expose telephone number
|
||||||
|
}
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: senderJID,
|
||||||
|
Username: senderName,
|
||||||
|
Channel: message.Info.RemoteJid,
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
|
ID: message.Info.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatarURL, exists := b.userAvatars[senderJID]; exists {
|
||||||
|
rmsg.Avatar = avatarURL
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExt, err := mime.ExtensionsByType(message.Type)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Mimetype detection error: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%v", message.FileName)
|
||||||
|
|
||||||
|
b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, message.Type)
|
||||||
|
|
||||||
|
data, err := message.Download()
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Download document message failed: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move file to bridge storage
|
||||||
|
helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General)
|
||||||
|
|
||||||
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
||||||
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
@ -6,22 +6,24 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
|
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
|
||||||
"github.com/Rhymen/go-whatsapp"
|
"github.com/Rhymen/go-whatsapp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProfilePicInfo struct {
|
type ProfilePicInfo struct {
|
||||||
URL string `json:"eurl"`
|
URL string `json:"eurl"`
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
|
Status int16 `json:"status"`
|
||||||
Status int16 `json:"status"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func qrFromTerminal(invert bool) chan string {
|
func qrFromTerminal(invert bool) chan string {
|
||||||
qr := make(chan string)
|
qr := make(chan string)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
terminal := qrcodeTerminal.New()
|
terminal := qrcodeTerminal.New()
|
||||||
|
|
||||||
if invert {
|
if invert {
|
||||||
terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium)
|
terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium)
|
||||||
}
|
}
|
||||||
@ -44,13 +46,12 @@ func (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return session, err
|
return session, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
decoder := gob.NewDecoder(file)
|
decoder := gob.NewDecoder(file)
|
||||||
err = decoder.Decode(&session)
|
|
||||||
if err != nil {
|
return session, decoder.Decode(&session)
|
||||||
return session, err
|
|
||||||
}
|
|
||||||
return session, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
|
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
|
||||||
@ -65,11 +66,31 @@ func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
|
||||||
encoder := gob.NewEncoder(file)
|
|
||||||
err = encoder.Encode(session)
|
|
||||||
|
|
||||||
return err
|
defer file.Close()
|
||||||
|
|
||||||
|
encoder := gob.NewEncoder(file)
|
||||||
|
|
||||||
|
return encoder.Encode(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) restoreSession() (*whatsapp.Session, error) {
|
||||||
|
session, err := b.readSession()
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Warn(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugln("Restoring WhatsApp session..")
|
||||||
|
|
||||||
|
session, err = b.conn.RestoreWithSession(session)
|
||||||
|
if err != nil {
|
||||||
|
// restore session connection timed out (I couldn't get over it without logging in again)
|
||||||
|
return nil, errors.New("failed to restore session: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugln("Session restored successfully!")
|
||||||
|
|
||||||
|
return &session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bwhatsapp) getSenderName(senderJid string) string {
|
func (b *Bwhatsapp) getSenderName(senderJid string) string {
|
||||||
@ -90,8 +111,7 @@ func (b *Bwhatsapp) getSenderName(senderJid string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// try to reload this contact
|
// try to reload this contact
|
||||||
_, err := b.conn.Contacts()
|
if _, err := b.conn.Contacts(); err != nil {
|
||||||
if err != nil {
|
|
||||||
b.Log.Errorf("error on update of contacts: %v", err)
|
b.Log.Errorf("error on update of contacts: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +134,7 @@ func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
|
|||||||
if sender, exists := b.users[senderJid]; exists {
|
if sender, exists := b.users[senderJid]; exists {
|
||||||
return sender.Notify
|
return sender.Notify
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,11 +143,20 @@ func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get avatar: %v", err)
|
return nil, fmt.Errorf("failed to get avatar: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
content := <-data
|
content := <-data
|
||||||
info := &ProfilePicInfo{}
|
info := &ProfilePicInfo{}
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(content), info)
|
err = json.Unmarshal([]byte(content), info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
|
return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isGroupJid(identifier string) bool {
|
||||||
|
return strings.HasSuffix(identifier, "@g.us") ||
|
||||||
|
strings.HasSuffix(identifier, "@temp") ||
|
||||||
|
strings.HasSuffix(identifier, "@broadcast")
|
||||||
|
}
|
||||||
|
@ -28,7 +28,6 @@ const (
|
|||||||
type Bwhatsapp struct {
|
type Bwhatsapp struct {
|
||||||
*bridge.Config
|
*bridge.Config
|
||||||
|
|
||||||
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21
|
|
||||||
session *whatsapp.Session
|
session *whatsapp.Session
|
||||||
conn *whatsapp.Conn
|
conn *whatsapp.Conn
|
||||||
startedAt uint64
|
startedAt uint64
|
||||||
@ -40,6 +39,12 @@ type Bwhatsapp struct {
|
|||||||
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
|
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
|
||||||
func New(cfg *bridge.Config) bridge.Bridger {
|
func New(cfg *bridge.Config) bridge.Bridger {
|
||||||
number := cfg.GetString(cfgNumber)
|
number := cfg.GetString(cfgNumber)
|
||||||
|
|
||||||
|
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 == "" {
|
if number == "" {
|
||||||
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
|
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
|
||||||
}
|
}
|
||||||
@ -50,21 +55,17 @@ func New(cfg *bridge.Config) bridge.Bridger {
|
|||||||
users: make(map[string]whatsapp.Contact),
|
users: make(map[string]whatsapp.Contact),
|
||||||
userAvatars: make(map[string]string),
|
userAvatars: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to WhatsApp. Required implementation of the Bridger interface
|
// Connect to WhatsApp. Required implementation of the Bridger interface
|
||||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
|
|
||||||
func (b *Bwhatsapp) Connect() error {
|
func (b *Bwhatsapp) Connect() error {
|
||||||
b.RLock() // TODO do we need locking for Whatsapp?
|
|
||||||
defer b.RUnlock()
|
|
||||||
|
|
||||||
number := b.GetString(cfgNumber)
|
number := b.GetString(cfgNumber)
|
||||||
if number == "" {
|
if number == "" {
|
||||||
return errors.New("WhatsApp's telephone Number need to be configured")
|
return errors.New("whatsapp's telephone number need to be configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/Rhymen/go-whatsapp#creating-a-connection
|
|
||||||
b.Log.Debugln("Connecting to WhatsApp..")
|
b.Log.Debugln("Connecting to WhatsApp..")
|
||||||
conn, err := whatsapp.NewConn(20 * time.Second)
|
conn, err := whatsapp.NewConn(20 * time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -77,35 +78,18 @@ func (b *Bwhatsapp) Connect() error {
|
|||||||
b.Log.Debugln("WhatsApp connection successful")
|
b.Log.Debugln("WhatsApp connection successful")
|
||||||
|
|
||||||
// load existing session in order to keep it between restarts
|
// load existing session in order to keep it between restarts
|
||||||
if b.session == nil {
|
b.session, err = b.restoreSession()
|
||||||
var session whatsapp.Session
|
if err != nil {
|
||||||
session, err = b.readSession()
|
b.Log.Warn(err.Error())
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
b.Log.Debugln("Restoring WhatsApp session..")
|
|
||||||
|
|
||||||
// https://github.com/Rhymen/go-whatsapp#restore
|
|
||||||
session, err = b.conn.RestoreWithSession(session)
|
|
||||||
if err != nil {
|
|
||||||
// TODO return or continue to normal login?
|
|
||||||
// restore session connection timed out (I couldn't get over it without logging in again)
|
|
||||||
return errors.New("failed to restore session: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
b.session = &session
|
|
||||||
b.Log.Debugln("Session restored successfully!")
|
|
||||||
} else {
|
|
||||||
b.Log.Warn(err.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// login to a new session
|
// login to a new session
|
||||||
if b.session == nil {
|
if b.session == nil {
|
||||||
err = b.Login()
|
if err = b.Login(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.startedAt = uint64(time.Now().Unix())
|
b.startedAt = uint64(time.Now().Unix())
|
||||||
|
|
||||||
_, err = b.conn.Contacts()
|
_, err = b.conn.Contacts()
|
||||||
@ -116,6 +100,7 @@ func (b *Bwhatsapp) Connect() error {
|
|||||||
// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
|
// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
|
||||||
for len(b.conn.Store.Contacts) == 0 {
|
for len(b.conn.Store.Contacts) == 0 {
|
||||||
b.conn.Contacts() // nolint:errcheck
|
b.conn.Contacts() // nolint:errcheck
|
||||||
|
|
||||||
<-time.After(1 * time.Second)
|
<-time.After(1 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,12 +120,13 @@ func (b *Bwhatsapp) Connect() error {
|
|||||||
info, err := b.GetProfilePicThumb(jid)
|
info, err := b.GetProfilePicThumb(jid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
|
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// TODO any race conditions here?
|
b.Lock()
|
||||||
b.userAvatars[jid] = info.URL
|
b.userAvatars[jid] = info.URL
|
||||||
|
b.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Log.Debug("Finished getting avatars..")
|
b.Log.Debug("Finished getting avatars..")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -157,8 +143,10 @@ func (b *Bwhatsapp) Login() error {
|
|||||||
session, err := b.conn.Login(qrChan)
|
session, err := b.conn.Login(qrChan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Log.Warnln("Failed to log in:", err)
|
b.Log.Warnln("Failed to log in:", err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.session = &session
|
b.session = &session
|
||||||
|
|
||||||
b.Log.Infof("Logged into session: %#v", session)
|
b.Log.Infof("Logged into session: %#v", session)
|
||||||
@ -169,29 +157,17 @@ func (b *Bwhatsapp) Login() error {
|
|||||||
fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO change connection strings to configured ones longClientName:"github.com/rhymen/go-whatsapp", shortClientName:"go-whatsapp"}" prefix=whatsapp
|
|
||||||
// TODO get also a nice logo
|
|
||||||
|
|
||||||
// TODO notification about unplugged and dead battery
|
|
||||||
// conn.Info: Wid, Pushname, Connected, Battery, Plugged
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect is called while reconnecting to the bridge
|
// Disconnect is called while reconnecting to the bridge
|
||||||
// TODO 42wim Documentation would be helpful on when reconnects happen and what should be done in this function
|
|
||||||
// Required implementation of the Bridger interface
|
// Required implementation of the Bridger interface
|
||||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
|
|
||||||
func (b *Bwhatsapp) Disconnect() error {
|
func (b *Bwhatsapp) Disconnect() error {
|
||||||
// We could Logout, but that would close the session completely and would require a new QR code scan
|
// We could Logout, but that would close the session completely and would require a new QR code scan
|
||||||
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
|
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isGroupJid(identifier string) bool {
|
|
||||||
return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp") || strings.HasSuffix(identifier, "@broadcast")
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
|
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
|
||||||
// Required implementation of the Bridger interface
|
// Required implementation of the Bridger interface
|
||||||
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
|
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
|
||||||
@ -210,39 +186,33 @@ func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
|
|||||||
if _, exists := b.conn.Store.Contacts[channel.Name]; !exists {
|
if _, exists := b.conn.Store.Contacts[channel.Name]; !exists {
|
||||||
return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name)
|
return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// channel.Name specifies group name that might change, warn about it
|
|
||||||
var jids []string
|
|
||||||
for id, contact := range b.conn.Store.Contacts {
|
|
||||||
if isGroupJid(id) && contact.Name == channel.Name {
|
|
||||||
jids = append(jids, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(jids) {
|
return nil
|
||||||
case 0:
|
}
|
||||||
// didn't match any group - print out possibilites
|
|
||||||
// TODO sort
|
|
||||||
// copy b;
|
|
||||||
//sort.Slice(people, func(i, j int) bool {
|
|
||||||
// return people[i].Age > people[j].Age
|
|
||||||
//})
|
|
||||||
for id, contact := range b.conn.Store.Contacts {
|
|
||||||
if isGroupJid(id) {
|
|
||||||
b.Log.Infof("%s %s", contact.Jid, contact.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
|
|
||||||
|
|
||||||
case 1:
|
// channel.Name specifies group name that might change, warn about it
|
||||||
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
|
var jids []string
|
||||||
|
for id, contact := range b.conn.Store.Contacts {
|
||||||
default:
|
if isGroupJid(id) && contact.Name == channel.Name {
|
||||||
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
|
jids = append(jids, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
switch len(jids) {
|
||||||
|
case 0:
|
||||||
|
// didn't match any group - print out possibilites
|
||||||
|
for id, contact := range b.conn.Store.Contacts {
|
||||||
|
if isGroupJid(id) {
|
||||||
|
b.Log.Infof("%s %s", contact.Jid, contact.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
|
||||||
|
case 1:
|
||||||
|
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post a document message from the bridge to WhatsApp
|
// Post a document message from the bridge to WhatsApp
|
||||||
@ -316,22 +286,23 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
|
|||||||
if msg.ID == "" {
|
if msg.ID == "" {
|
||||||
// No message ID in case action is executed on a message sent before the bridge was started
|
// No message ID in case action is executed on a message sent before the bridge was started
|
||||||
// and then the bridge cache doesn't have this message ID mapped
|
// and then the bridge cache doesn't have this message ID mapped
|
||||||
|
|
||||||
// TODO 42wim Doesn't the app get clogged with a ton of IDs after some time of running?
|
|
||||||
// WhatsApp allows to set any ID so in that case we could use external IDs and don't do mapping
|
|
||||||
// but external IDs are not set
|
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
// TODO delete message on WhatsApp https://github.com/Rhymen/go-whatsapp/issues/100
|
|
||||||
return "", nil
|
_, err := b.conn.RevokeMessage(msg.Channel, msg.ID, true)
|
||||||
|
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit message
|
// Edit message
|
||||||
if msg.ID != "" {
|
if msg.ID != "" {
|
||||||
b.Log.Debugf("updating message with id %s", msg.ID)
|
b.Log.Debugf("updating message with id %s", msg.ID)
|
||||||
|
|
||||||
msg.Text += " (edited)"
|
if b.GetString("editsuffix") != "" {
|
||||||
// TODO handle edit as a message reply with updated text
|
msg.Text += b.GetString("EditSuffix")
|
||||||
|
} else {
|
||||||
|
msg.Text += " (edited)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Upload a file
|
// Handle Upload a file
|
||||||
@ -361,16 +332,7 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
|
|||||||
|
|
||||||
b.Log.Debugf("=> Sending %#v", msg)
|
b.Log.Debugf("=> Sending %#v", msg)
|
||||||
|
|
||||||
// create message ID
|
return b.conn.Send(message)
|
||||||
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76
|
// TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76
|
||||||
|
454
bridge/whatsappmulti/handlers.go
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
//go:build whatsappmulti
|
||||||
|
// +build whatsappmulti
|
||||||
|
|
||||||
|
package bwhatsapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
|
|
||||||
|
"go.mau.fi/whatsmeow/binary/proto"
|
||||||
|
"go.mau.fi/whatsmeow/types"
|
||||||
|
"go.mau.fi/whatsmeow/types/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint:gocritic
|
||||||
|
func (b *Bwhatsapp) eventHandler(evt interface{}) {
|
||||||
|
switch e := evt.(type) {
|
||||||
|
case *events.Message:
|
||||||
|
b.handleMessage(e)
|
||||||
|
case *events.GroupInfo:
|
||||||
|
b.handleGroupInfo(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) handleGroupInfo(event *events.GroupInfo) {
|
||||||
|
b.Log.Debugf("Receiving event %#v", event)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case event.Join != nil:
|
||||||
|
b.handleUserJoin(event)
|
||||||
|
case event.Leave != nil:
|
||||||
|
b.handleUserLeave(event)
|
||||||
|
case event.Topic != nil:
|
||||||
|
b.handleTopicChange(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) handleUserJoin(event *events.GroupInfo) {
|
||||||
|
for _, joinedJid := range event.Join {
|
||||||
|
senderName := b.getSenderNameFromJID(joinedJid)
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: joinedJid.String(),
|
||||||
|
Username: senderName,
|
||||||
|
Channel: event.JID.String(),
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Event: config.EventJoinLeave,
|
||||||
|
Text: "joined chat",
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) handleUserLeave(event *events.GroupInfo) {
|
||||||
|
for _, leftJid := range event.Leave {
|
||||||
|
senderName := b.getSenderNameFromJID(leftJid)
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: leftJid.String(),
|
||||||
|
Username: senderName,
|
||||||
|
Channel: event.JID.String(),
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Event: config.EventJoinLeave,
|
||||||
|
Text: "left chat",
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) handleTopicChange(event *events.GroupInfo) {
|
||||||
|
msg := event.Topic
|
||||||
|
senderJid := msg.TopicSetBy
|
||||||
|
senderName := b.getSenderNameFromJID(senderJid)
|
||||||
|
|
||||||
|
text := msg.Topic
|
||||||
|
if text == "" {
|
||||||
|
text = "removed topic"
|
||||||
|
}
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: senderJid.String(),
|
||||||
|
Username: senderName,
|
||||||
|
Channel: event.JID.String(),
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Event: config.EventTopicChange,
|
||||||
|
Text: "Topic changed: " + text,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) handleMessage(message *events.Message) {
|
||||||
|
msg := message.Message
|
||||||
|
switch {
|
||||||
|
case msg == nil, message.Info.IsFromMe, message.Info.Timestamp.Before(b.startedAt):
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugf("Receiving message %#v", msg)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case msg.Conversation != nil || msg.ExtendedTextMessage != nil:
|
||||||
|
b.handleTextMessage(message.Info, msg)
|
||||||
|
case msg.VideoMessage != nil:
|
||||||
|
b.handleVideoMessage(message)
|
||||||
|
case msg.AudioMessage != nil:
|
||||||
|
b.handleAudioMessage(message)
|
||||||
|
case msg.DocumentMessage != nil:
|
||||||
|
b.handleDocumentMessage(message)
|
||||||
|
case msg.ImageMessage != nil:
|
||||||
|
b.handleImageMessage(message)
|
||||||
|
case msg.ProtocolMessage != nil && *msg.ProtocolMessage.Type == proto.ProtocolMessage_REVOKE:
|
||||||
|
b.handleDelete(msg.ProtocolMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:funlen
|
||||||
|
func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.Message) {
|
||||||
|
senderJID := messageInfo.Sender
|
||||||
|
channel := messageInfo.Chat
|
||||||
|
|
||||||
|
senderName := b.getSenderName(messageInfo)
|
||||||
|
|
||||||
|
if msg.GetExtendedTextMessage() == nil && msg.GetConversation() == "" {
|
||||||
|
b.Log.Debugf("message without text content? %#v", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var text string
|
||||||
|
|
||||||
|
// nolint:nestif
|
||||||
|
if msg.GetExtendedTextMessage() == nil {
|
||||||
|
text = msg.GetConversation()
|
||||||
|
} else if msg.GetExtendedTextMessage().GetContextInfo() == nil {
|
||||||
|
// Handle pure text message with a link preview
|
||||||
|
// A pure text message with a link preview acts as an extended text message but will not contain any context info
|
||||||
|
text = msg.GetExtendedTextMessage().GetText()
|
||||||
|
} else {
|
||||||
|
text = msg.GetExtendedTextMessage().GetText()
|
||||||
|
ci := msg.GetExtendedTextMessage().GetContextInfo()
|
||||||
|
|
||||||
|
if senderJID == (types.JID{}) && ci.Participant != nil {
|
||||||
|
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ci.MentionedJID != nil {
|
||||||
|
// handle user mentions
|
||||||
|
for _, mentionedJID := range ci.MentionedJID {
|
||||||
|
numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
|
||||||
|
|
||||||
|
// mentions comes as telephone numbers and we don't want to expose it to other bridges
|
||||||
|
// replace it with something more meaninful to others
|
||||||
|
mention := b.getSenderNotify(types.NewJID(numberAndSuffix[0], types.DefaultUserServer))
|
||||||
|
|
||||||
|
text = strings.Replace(text, "@"+numberAndSuffix[0], "@"+mention, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parentID := ""
|
||||||
|
if msg.GetExtendedTextMessage() != nil {
|
||||||
|
ci := msg.GetExtendedTextMessage().GetContextInfo()
|
||||||
|
parentID = getParentIdFromCtx(ci)
|
||||||
|
}
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: senderJID.String(),
|
||||||
|
Username: senderName,
|
||||||
|
Text: text,
|
||||||
|
Channel: channel.String(),
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
|
ID: getMessageIdFormat(senderJID, messageInfo.ID),
|
||||||
|
ParentID: parentID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
|
||||||
|
rmsg.Avatar = avatarURL
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
||||||
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleImageMessage sent from WhatsApp, relay it to the brige
|
||||||
|
func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
|
||||||
|
imsg := msg.Message.GetImageMessage()
|
||||||
|
|
||||||
|
senderJID := msg.Info.Sender
|
||||||
|
senderName := b.getSenderName(msg.Info)
|
||||||
|
ci := imsg.GetContextInfo()
|
||||||
|
|
||||||
|
if senderJID == (types.JID{}) && ci.Participant != nil {
|
||||||
|
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: senderJID.String(),
|
||||||
|
Username: senderName,
|
||||||
|
Channel: msg.Info.Chat.String(),
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
|
ID: getMessageIdFormat(senderJID, msg.Info.ID),
|
||||||
|
ParentID: getParentIdFromCtx(ci),
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
|
||||||
|
rmsg.Avatar = avatarURL
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Mimetype detection error: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
|
||||||
|
if fileExt[0] == ".jfif" {
|
||||||
|
fileExt[0] = ".jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463
|
||||||
|
if fileExt[0] == ".jpe" {
|
||||||
|
fileExt[0] = ".jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
|
||||||
|
|
||||||
|
b.Log.Debugf("Trying to download %s with type %s", filename, imsg.GetMimetype())
|
||||||
|
|
||||||
|
data, err := b.wc.Download(imsg)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Download image failed: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move file to bridge storage
|
||||||
|
helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
|
||||||
|
|
||||||
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
||||||
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleVideoMessage downloads video messages
|
||||||
|
func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
|
||||||
|
imsg := msg.Message.GetVideoMessage()
|
||||||
|
|
||||||
|
senderJID := msg.Info.Sender
|
||||||
|
senderName := b.getSenderName(msg.Info)
|
||||||
|
ci := imsg.GetContextInfo()
|
||||||
|
|
||||||
|
if senderJID == (types.JID{}) && ci.Participant != nil {
|
||||||
|
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: senderJID.String(),
|
||||||
|
Username: senderName,
|
||||||
|
Channel: msg.Info.Chat.String(),
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
|
ID: getMessageIdFormat(senderJID, msg.Info.ID),
|
||||||
|
ParentID: getParentIdFromCtx(ci),
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
|
||||||
|
rmsg.Avatar = avatarURL
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Mimetype detection error: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fileExt) == 0 {
|
||||||
|
fileExt = append(fileExt, ".mp4")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer .mp4 extension, otherwise fallback to first index
|
||||||
|
fileExtIndex := 0
|
||||||
|
for i, n := range fileExt {
|
||||||
|
if ".mp4" == n {
|
||||||
|
fileExtIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[fileExtIndex])
|
||||||
|
|
||||||
|
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
|
||||||
|
|
||||||
|
data, err := b.wc.Download(imsg)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Download video failed: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move file to bridge storage
|
||||||
|
helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
|
||||||
|
|
||||||
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
||||||
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleAudioMessage downloads audio messages
|
||||||
|
func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
|
||||||
|
imsg := msg.Message.GetAudioMessage()
|
||||||
|
|
||||||
|
senderJID := msg.Info.Sender
|
||||||
|
senderName := b.getSenderName(msg.Info)
|
||||||
|
ci := imsg.GetContextInfo()
|
||||||
|
|
||||||
|
if senderJID == (types.JID{}) && ci.Participant != nil {
|
||||||
|
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
|
||||||
|
}
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: senderJID.String(),
|
||||||
|
Username: senderName,
|
||||||
|
Channel: msg.Info.Chat.String(),
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
|
ID: getMessageIdFormat(senderJID, msg.Info.ID),
|
||||||
|
ParentID: getParentIdFromCtx(ci),
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
|
||||||
|
rmsg.Avatar = avatarURL
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Mimetype detection error: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fileExt) == 0 {
|
||||||
|
fileExt = append(fileExt, ".ogg")
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
|
||||||
|
|
||||||
|
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
|
||||||
|
|
||||||
|
data, err := b.wc.Download(imsg)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Download video failed: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move file to bridge storage
|
||||||
|
helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
|
||||||
|
|
||||||
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
||||||
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleDocumentMessage downloads documents
|
||||||
|
func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) {
|
||||||
|
imsg := msg.Message.GetDocumentMessage()
|
||||||
|
|
||||||
|
senderJID := msg.Info.Sender
|
||||||
|
senderName := b.getSenderName(msg.Info)
|
||||||
|
ci := imsg.GetContextInfo()
|
||||||
|
|
||||||
|
if senderJID == (types.JID{}) && ci.Participant != nil {
|
||||||
|
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
UserID: senderJID.String(),
|
||||||
|
Username: senderName,
|
||||||
|
Channel: msg.Info.Chat.String(),
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
Extra: make(map[string][]interface{}),
|
||||||
|
ID: getMessageIdFormat(senderJID, msg.Info.ID),
|
||||||
|
ParentID: getParentIdFromCtx(ci),
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
|
||||||
|
rmsg.Avatar = avatarURL
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Mimetype detection error: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%v", imsg.GetFileName())
|
||||||
|
|
||||||
|
b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, imsg.GetMimetype())
|
||||||
|
|
||||||
|
data, err := b.wc.Download(imsg)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Download document message failed: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move file to bridge storage
|
||||||
|
helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
|
||||||
|
|
||||||
|
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
|
||||||
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) handleDelete(messageInfo *proto.ProtocolMessage) {
|
||||||
|
sender, _ := types.ParseJID(*messageInfo.Key.Participant)
|
||||||
|
|
||||||
|
rmsg := config.Message{
|
||||||
|
Account: b.Account,
|
||||||
|
Protocol: b.Protocol,
|
||||||
|
ID: getMessageIdFormat(sender, *messageInfo.Key.ID),
|
||||||
|
Event: config.EventMsgDelete,
|
||||||
|
Text: config.EventMsgDelete,
|
||||||
|
Channel: *messageInfo.Key.RemoteJID,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
|
||||||
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
209
bridge/whatsappmulti/helpers.go
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
//go:build whatsappmulti
|
||||||
|
// +build whatsappmulti
|
||||||
|
|
||||||
|
package bwhatsapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
goproto "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"go.mau.fi/whatsmeow"
|
||||||
|
"go.mau.fi/whatsmeow/binary/proto"
|
||||||
|
"go.mau.fi/whatsmeow/store"
|
||||||
|
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||||
|
"go.mau.fi/whatsmeow/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfilePicInfo struct {
|
||||||
|
URL string `json:"eurl"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Status int16 `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) reloadContacts() {
|
||||||
|
if _, err := b.wc.Store.Contacts.GetAllContacts(); err != nil {
|
||||||
|
b.Log.Errorf("error on update of contacts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allcontacts, err := b.wc.Store.Contacts.GetAllContacts()
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("error on update of contacts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allcontacts) > 0 {
|
||||||
|
b.contacts = allcontacts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) getSenderName(info types.MessageInfo) string {
|
||||||
|
// Parse AD JID
|
||||||
|
var senderJid types.JID
|
||||||
|
senderJid.User, senderJid.Server = info.Sender.User, info.Sender.Server
|
||||||
|
|
||||||
|
sender, exists := b.contacts[senderJid]
|
||||||
|
|
||||||
|
if !exists || (sender.FullName == "" && sender.FirstName == "") {
|
||||||
|
b.reloadContacts() // Contacts may need to be reloaded
|
||||||
|
sender, exists = b.contacts[senderJid]
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists && sender.FullName != "" {
|
||||||
|
return sender.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.PushName != "" {
|
||||||
|
return info.PushName
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists && sender.FirstName != "" {
|
||||||
|
return sender.FirstName
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Someone"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) getSenderNameFromJID(senderJid types.JID) string {
|
||||||
|
sender, exists := b.contacts[senderJid]
|
||||||
|
|
||||||
|
if !exists || (sender.FullName == "" && sender.FirstName == "") {
|
||||||
|
b.reloadContacts() // Contacts may need to be reloaded
|
||||||
|
sender, exists = b.contacts[senderJid]
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists && sender.FullName != "" {
|
||||||
|
return sender.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists && sender.FirstName != "" {
|
||||||
|
return sender.FirstName
|
||||||
|
}
|
||||||
|
|
||||||
|
if sender.PushName != "" {
|
||||||
|
return sender.PushName
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Someone"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) getSenderNotify(senderJid types.JID) string {
|
||||||
|
sender, exists := b.contacts[senderJid]
|
||||||
|
|
||||||
|
if !exists || (sender.FullName == "" && sender.PushName == "" && sender.FirstName == "") {
|
||||||
|
b.reloadContacts() // Contacts may need to be reloaded
|
||||||
|
sender, exists = b.contacts[senderJid]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return "someone"
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists && sender.FullName != "" {
|
||||||
|
return sender.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists && sender.PushName != "" {
|
||||||
|
return sender.PushName
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists && sender.FirstName != "" {
|
||||||
|
return sender.FirstName
|
||||||
|
}
|
||||||
|
|
||||||
|
return "someone"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*types.ProfilePictureInfo, error) {
|
||||||
|
pjid, _ := types.ParseJID(jid)
|
||||||
|
|
||||||
|
info, err := b.wc.GetProfilePictureInfo(pjid, &whatsmeow.GetProfilePictureParams{
|
||||||
|
Preview: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get avatar: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGroupJid(identifier string) bool {
|
||||||
|
return strings.HasSuffix(identifier, "@g.us") ||
|
||||||
|
strings.HasSuffix(identifier, "@temp") ||
|
||||||
|
strings.HasSuffix(identifier, "@broadcast")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) getDevice() (*store.Device, error) {
|
||||||
|
device := &store.Device{}
|
||||||
|
|
||||||
|
storeContainer, err := sqlstore.New("sqlite", "file:"+b.Config.GetString("sessionfile")+".db?_pragma=foreign_keys(1)&_pragma=busy_timeout=10000", nil)
|
||||||
|
if err != nil {
|
||||||
|
return device, fmt.Errorf("failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err = storeContainer.GetFirstDevice()
|
||||||
|
if err != nil {
|
||||||
|
return device, fmt.Errorf("failed to get device: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return device, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) getNewReplyContext(parentID string) (*proto.ContextInfo, error) {
|
||||||
|
replyInfo, err := b.parseMessageID(parentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sender := fmt.Sprintf("%s@%s", replyInfo.Sender.User, replyInfo.Sender.Server)
|
||||||
|
ctx := &proto.ContextInfo{
|
||||||
|
StanzaID: &replyInfo.MessageID,
|
||||||
|
Participant: &sender,
|
||||||
|
QuotedMessage: &proto.Message{Conversation: goproto.String("")},
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) parseMessageID(id string) (*Replyable, error) {
|
||||||
|
// No message ID in case action is executed on a message sent before the bridge was started
|
||||||
|
// and then the bridge cache doesn't have this message ID mapped
|
||||||
|
if id == "" {
|
||||||
|
return &Replyable{MessageID: id}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
replyInfo := strings.Split(id, "/")
|
||||||
|
|
||||||
|
if len(replyInfo) == 2 {
|
||||||
|
sender, err := types.ParseJID(replyInfo[0])
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return &Replyable{
|
||||||
|
MessageID: types.MessageID(replyInfo[1]),
|
||||||
|
Sender: sender,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := fmt.Errorf("MessageID does not match format of {senderJID}:{messageID} : \"%s\"", id)
|
||||||
|
|
||||||
|
return &Replyable{MessageID: id}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getParentIdFromCtx(ci *proto.ContextInfo) string {
|
||||||
|
if ci != nil && ci.StanzaID != nil {
|
||||||
|
senderJid, err := types.ParseJID(*ci.Participant)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return getMessageIdFormat(senderJid, *ci.StanzaID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMessageIdFormat(jid types.JID, messageID string) string {
|
||||||
|
// we're crafting our own JID str as AD JID format messes with how stuff looks on a webclient
|
||||||
|
jidStr := fmt.Sprintf("%s@%s", jid.User, jid.Server)
|
||||||
|
return fmt.Sprintf("%s/%s", jidStr, messageID)
|
||||||
|
}
|
456
bridge/whatsappmulti/whatsapp.go
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
//go:build whatsappmulti
|
||||||
|
// +build whatsappmulti
|
||||||
|
|
||||||
|
package bwhatsapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/42wim/matterbridge/bridge"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/mdp/qrterminal"
|
||||||
|
|
||||||
|
"go.mau.fi/whatsmeow"
|
||||||
|
"go.mau.fi/whatsmeow/binary/proto"
|
||||||
|
"go.mau.fi/whatsmeow/types"
|
||||||
|
waLog "go.mau.fi/whatsmeow/util/log"
|
||||||
|
|
||||||
|
goproto "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite" // needed for sqlite
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Account config parameters
|
||||||
|
cfgNumber = "Number"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bwhatsapp Bridge structure keeping all the information needed for relying
|
||||||
|
type Bwhatsapp struct {
|
||||||
|
*bridge.Config
|
||||||
|
|
||||||
|
startedAt time.Time
|
||||||
|
wc *whatsmeow.Client
|
||||||
|
contacts map[types.JID]types.ContactInfo
|
||||||
|
users map[string]types.ContactInfo
|
||||||
|
userAvatars map[string]string
|
||||||
|
joinedGroups []*types.GroupInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type Replyable struct {
|
||||||
|
MessageID types.MessageID
|
||||||
|
Sender types.JID
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
|
||||||
|
func New(cfg *bridge.Config) bridge.Bridger {
|
||||||
|
number := cfg.GetString(cfgNumber)
|
||||||
|
|
||||||
|
if number == "" {
|
||||||
|
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &Bwhatsapp{
|
||||||
|
Config: cfg,
|
||||||
|
|
||||||
|
users: make(map[string]types.ContactInfo),
|
||||||
|
userAvatars: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to WhatsApp. Required implementation of the Bridger interface
|
||||||
|
func (b *Bwhatsapp) Connect() error {
|
||||||
|
device, err := b.getDevice()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
number := b.GetString(cfgNumber)
|
||||||
|
if number == "" {
|
||||||
|
return errors.New("whatsapp's telephone number need to be configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugln("Connecting to WhatsApp..")
|
||||||
|
|
||||||
|
b.wc = whatsmeow.NewClient(device, waLog.Stdout("Client", "INFO", true))
|
||||||
|
b.wc.AddEventHandler(b.eventHandler)
|
||||||
|
|
||||||
|
firstlogin := false
|
||||||
|
var qrChan <-chan whatsmeow.QRChannelItem
|
||||||
|
if b.wc.Store.ID == nil {
|
||||||
|
firstlogin = true
|
||||||
|
qrChan, err = b.wc.GetQRChannel(context.Background())
|
||||||
|
if err != nil && !errors.Is(err, whatsmeow.ErrQRStoreContainsID) {
|
||||||
|
return errors.New("failed to to get QR channel:" + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.wc.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to connect to WhatsApp: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.wc.Store.ID == nil {
|
||||||
|
for evt := range qrChan {
|
||||||
|
if evt.Event == "code" {
|
||||||
|
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
|
||||||
|
} else {
|
||||||
|
b.Log.Infof("QR channel result: %s", evt.Event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// disconnect and reconnect on our first login/pairing
|
||||||
|
// for some reason the GetJoinedGroups in JoinChannel doesn't work on first login
|
||||||
|
if firstlogin {
|
||||||
|
b.wc.Disconnect()
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
err = b.wc.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to connect to WhatsApp: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Infoln("WhatsApp connection successful")
|
||||||
|
|
||||||
|
b.contacts, err = b.wc.Store.Contacts.GetAllContacts()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to get contacts: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
b.joinedGroups, err = b.wc.GetJoinedGroups()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to get list of joined groups: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
b.startedAt = time.Now()
|
||||||
|
|
||||||
|
// map all the users
|
||||||
|
for id, contact := range b.contacts {
|
||||||
|
if !isGroupJid(id.String()) && id.String() != "status@broadcast" {
|
||||||
|
// it is user
|
||||||
|
b.users[id.String()] = contact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get user avatar asynchronously
|
||||||
|
b.Log.Info("Getting user avatars..")
|
||||||
|
|
||||||
|
for jid := range b.users {
|
||||||
|
info, err := b.GetProfilePicThumb(jid)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
|
||||||
|
} else {
|
||||||
|
b.Lock()
|
||||||
|
if info != nil {
|
||||||
|
b.userAvatars[jid] = info.URL
|
||||||
|
}
|
||||||
|
b.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Info("Finished getting avatars..")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect is called while reconnecting to the bridge
|
||||||
|
// Required implementation of the Bridger interface
|
||||||
|
func (b *Bwhatsapp) Disconnect() error {
|
||||||
|
b.wc.Disconnect()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
|
||||||
|
// Required implementation of the Bridger interface
|
||||||
|
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
|
||||||
|
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
byJid := isGroupJid(channel.Name)
|
||||||
|
|
||||||
|
// verify if we are member of the given group
|
||||||
|
if byJid {
|
||||||
|
gJID, err := types.ParseJID(channel.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range b.joinedGroups {
|
||||||
|
if group.JID == gJID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foundGroups := []string{}
|
||||||
|
|
||||||
|
for _, group := range b.joinedGroups {
|
||||||
|
if group.Name == channel.Name {
|
||||||
|
foundGroups = append(foundGroups, group.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(foundGroups) {
|
||||||
|
case 0:
|
||||||
|
// didn't match any group - print out possibilites
|
||||||
|
for _, group := range b.joinedGroups {
|
||||||
|
b.Log.Infof("%s %s", group.JID, group.Name)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
|
||||||
|
case 1:
|
||||||
|
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", foundGroups[0], channel.Name)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, foundGroups)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post a document message from the bridge to WhatsApp
|
||||||
|
func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
|
||||||
|
groupJID, _ := types.ParseJID(msg.Channel)
|
||||||
|
|
||||||
|
fi := msg.Extra["file"][0].(config.FileInfo)
|
||||||
|
|
||||||
|
caption := msg.Username + fi.Comment
|
||||||
|
|
||||||
|
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaDocument)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post document message
|
||||||
|
var message proto.Message
|
||||||
|
var ctx *proto.ContextInfo
|
||||||
|
if msg.ParentID != "" {
|
||||||
|
ctx, _ = b.getNewReplyContext(msg.ParentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
message.DocumentMessage = &proto.DocumentMessage{
|
||||||
|
Title: &fi.Name,
|
||||||
|
FileName: &fi.Name,
|
||||||
|
Mimetype: &filetype,
|
||||||
|
Caption: &caption,
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: goproto.Uint64(resp.FileLength),
|
||||||
|
URL: &resp.URL,
|
||||||
|
DirectPath: &resp.DirectPath,
|
||||||
|
ContextInfo: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugf("=> Sending %#v as a document", msg)
|
||||||
|
|
||||||
|
ID := whatsmeow.GenerateMessageID()
|
||||||
|
_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
|
||||||
|
|
||||||
|
return ID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post an image message from the bridge to WhatsApp
|
||||||
|
// Handle, for sure image/jpeg, image/png and image/gif MIME types
|
||||||
|
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
|
||||||
|
fi := msg.Extra["file"][0].(config.FileInfo)
|
||||||
|
|
||||||
|
caption := msg.Username + fi.Comment
|
||||||
|
|
||||||
|
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaImage)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var message proto.Message
|
||||||
|
var ctx *proto.ContextInfo
|
||||||
|
if msg.ParentID != "" {
|
||||||
|
ctx, _ = b.getNewReplyContext(msg.ParentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
message.ImageMessage = &proto.ImageMessage{
|
||||||
|
Mimetype: &filetype,
|
||||||
|
Caption: &caption,
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: goproto.Uint64(resp.FileLength),
|
||||||
|
URL: &resp.URL,
|
||||||
|
DirectPath: &resp.DirectPath,
|
||||||
|
ContextInfo: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugf("=> Sending %#v as an image", msg)
|
||||||
|
|
||||||
|
return b.sendMessage(msg, &message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post a video message from the bridge to WhatsApp
|
||||||
|
func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (string, error) {
|
||||||
|
fi := msg.Extra["file"][0].(config.FileInfo)
|
||||||
|
|
||||||
|
caption := msg.Username + fi.Comment
|
||||||
|
|
||||||
|
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaVideo)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var message proto.Message
|
||||||
|
var ctx *proto.ContextInfo
|
||||||
|
if msg.ParentID != "" {
|
||||||
|
ctx, _ = b.getNewReplyContext(msg.ParentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
message.VideoMessage = &proto.VideoMessage{
|
||||||
|
Mimetype: &filetype,
|
||||||
|
Caption: &caption,
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: goproto.Uint64(resp.FileLength),
|
||||||
|
URL: &resp.URL,
|
||||||
|
DirectPath: &resp.DirectPath,
|
||||||
|
ContextInfo: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugf("=> Sending %#v as a video", msg)
|
||||||
|
|
||||||
|
return b.sendMessage(msg, &message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post audio inline
|
||||||
|
func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (string, error) {
|
||||||
|
groupJID, _ := types.ParseJID(msg.Channel)
|
||||||
|
|
||||||
|
fi := msg.Extra["file"][0].(config.FileInfo)
|
||||||
|
|
||||||
|
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaAudio)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var message proto.Message
|
||||||
|
var ctx *proto.ContextInfo
|
||||||
|
if msg.ParentID != "" {
|
||||||
|
ctx, _ = b.getNewReplyContext(msg.ParentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
message.AudioMessage = &proto.AudioMessage{
|
||||||
|
Mimetype: &filetype,
|
||||||
|
MediaKey: resp.MediaKey,
|
||||||
|
FileEncSHA256: resp.FileEncSHA256,
|
||||||
|
FileSHA256: resp.FileSHA256,
|
||||||
|
FileLength: goproto.Uint64(resp.FileLength),
|
||||||
|
URL: &resp.URL,
|
||||||
|
DirectPath: &resp.DirectPath,
|
||||||
|
ContextInfo: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Log.Debugf("=> Sending %#v as audio", msg)
|
||||||
|
|
||||||
|
ID, err := b.sendMessage(msg, &message)
|
||||||
|
|
||||||
|
var captionMessage proto.Message
|
||||||
|
caption := msg.Username + fi.Comment + "\u2B06" // the char on the end is upwards arrow emoji
|
||||||
|
captionMessage.Conversation = &caption
|
||||||
|
|
||||||
|
captionID := whatsmeow.GenerateMessageID()
|
||||||
|
_, err = b.wc.SendMessage(context.TODO(), groupJID, &captionMessage, whatsmeow.SendRequestExtra{ID: captionID})
|
||||||
|
|
||||||
|
return ID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a message from the bridge to WhatsApp
|
||||||
|
func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
|
||||||
|
groupJID, _ := types.ParseJID(msg.Channel)
|
||||||
|
|
||||||
|
extendedMsgID, _ := b.parseMessageID(msg.ID)
|
||||||
|
msg.ID = extendedMsgID.MessageID
|
||||||
|
|
||||||
|
b.Log.Debugf("=> Receiving %#v", msg)
|
||||||
|
|
||||||
|
// Delete message
|
||||||
|
if msg.Event == config.EventMsgDelete {
|
||||||
|
if msg.ID == "" {
|
||||||
|
// No message ID in case action is executed on a message sent before the bridge was started
|
||||||
|
// and then the bridge cache doesn't have this message ID mapped
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := b.wc.RevokeMessage(groupJID, msg.ID)
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit message
|
||||||
|
if msg.ID != "" {
|
||||||
|
b.Log.Debugf("updating message with id %s", msg.ID)
|
||||||
|
|
||||||
|
if b.GetString("editsuffix") != "" {
|
||||||
|
msg.Text += b.GetString("EditSuffix")
|
||||||
|
} else {
|
||||||
|
msg.Text += " (edited)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Upload a file
|
||||||
|
if msg.Extra["file"] != nil {
|
||||||
|
fi := msg.Extra["file"][0].(config.FileInfo)
|
||||||
|
filetype := mime.TypeByExtension(filepath.Ext(fi.Name))
|
||||||
|
|
||||||
|
b.Log.Debugf("Extra file is %#v", filetype)
|
||||||
|
|
||||||
|
// TODO: add different types
|
||||||
|
// TODO: add webp conversion
|
||||||
|
switch filetype {
|
||||||
|
case "image/jpeg", "image/png", "image/gif":
|
||||||
|
return b.PostImageMessage(msg, filetype)
|
||||||
|
case "video/mp4", "video/3gpp": // TODO: Check if codecs are supported by WA
|
||||||
|
return b.PostVideoMessage(msg, filetype)
|
||||||
|
case "audio/ogg":
|
||||||
|
return b.PostAudioMessage(msg, "audio/ogg; codecs=opus") // TODO: Detect if it is actually OPUS
|
||||||
|
case "audio/aac", "audio/mp4", "audio/amr", "audio/mpeg":
|
||||||
|
return b.PostAudioMessage(msg, filetype)
|
||||||
|
default:
|
||||||
|
return b.PostDocumentMessage(msg, filetype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var message proto.Message
|
||||||
|
text := msg.Username + msg.Text
|
||||||
|
|
||||||
|
// If we have a parent ID send an extended message
|
||||||
|
if msg.ParentID != "" {
|
||||||
|
replyContext, err := b.getNewReplyContext(msg.ParentID)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
message = proto.Message{
|
||||||
|
ExtendedTextMessage: &proto.ExtendedTextMessage{
|
||||||
|
Text: &text,
|
||||||
|
ContextInfo: replyContext,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.sendMessage(msg, &message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message.Conversation = &text
|
||||||
|
|
||||||
|
return b.sendMessage(msg, &message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bwhatsapp) sendMessage(rmsg config.Message, message *proto.Message) (string, error) {
|
||||||
|
groupJID, _ := types.ParseJID(rmsg.Channel)
|
||||||
|
ID := whatsmeow.GenerateMessageID()
|
||||||
|
|
||||||
|
_, err := b.wc.SendMessage(context.Background(), groupJID, message, whatsmeow.SendRequestExtra{ID: ID})
|
||||||
|
|
||||||
|
return getMessageIdFormat(*b.wc.Store.ID, ID), err
|
||||||
|
}
|
@ -1,8 +1,12 @@
|
|||||||
package bxmpp
|
package bxmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -10,6 +14,7 @@ import (
|
|||||||
"github.com/42wim/matterbridge/bridge"
|
"github.com/42wim/matterbridge/bridge"
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
|
lru "github.com/hashicorp/golang-lru"
|
||||||
"github.com/jpillora/backoff"
|
"github.com/jpillora/backoff"
|
||||||
"github.com/matterbridge/go-xmpp"
|
"github.com/matterbridge/go-xmpp"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
@ -24,13 +29,20 @@ type Bxmpp struct {
|
|||||||
connected bool
|
connected bool
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
|
||||||
|
StanzaIDs *lru.Cache
|
||||||
|
OriginIDs *lru.Cache
|
||||||
|
|
||||||
avatarAvailability map[string]bool
|
avatarAvailability map[string]bool
|
||||||
avatarMap map[string]string
|
avatarMap map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *bridge.Config) bridge.Bridger {
|
func New(cfg *bridge.Config) bridge.Bridger {
|
||||||
|
stanzaIDs, _ := lru.New(5000)
|
||||||
|
originIDs, _ := lru.New(5000)
|
||||||
return &Bxmpp{
|
return &Bxmpp{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
|
StanzaIDs: stanzaIDs,
|
||||||
|
OriginIDs: originIDs,
|
||||||
xmppMap: make(map[string]string),
|
xmppMap: make(map[string]string),
|
||||||
avatarAvailability: make(map[string]bool),
|
avatarAvailability: make(map[string]bool),
|
||||||
avatarMap: make(map[string]string),
|
avatarMap: make(map[string]string),
|
||||||
@ -86,14 +98,21 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support).
|
// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support).
|
||||||
|
var err error
|
||||||
if msg.Extra != nil {
|
if msg.Extra != nil {
|
||||||
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
|
||||||
b.Log.Debugf("=> Sending attachement message %#v", rmsg)
|
b.Log.Debugf("=> Sending attachement message %#v", rmsg)
|
||||||
if _, err := b.xc.Send(xmpp.Chat{
|
if b.GetString("WebhookURL") != "" {
|
||||||
Type: "groupchat",
|
err = b.postSlackCompatibleWebhook(msg)
|
||||||
Remote: rmsg.Channel + "@" + b.GetString("Muc"),
|
} else {
|
||||||
Text: rmsg.Username + rmsg.Text,
|
_, err = b.xc.Send(xmpp.Chat{
|
||||||
}); err != nil {
|
Type: "groupchat",
|
||||||
|
Remote: rmsg.Channel + "@" + b.GetString("Muc"),
|
||||||
|
Text: rmsg.Username + rmsg.Text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
b.Log.WithError(err).Error("Unable to send message with share URL.")
|
b.Log.WithError(err).Error("Unable to send message with share URL.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,13 +121,31 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.GetString("WebhookURL") != "" {
|
||||||
|
b.Log.Debugf("Sending message using Webhook")
|
||||||
|
err := b.postSlackCompatibleWebhook(msg)
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Failed to send message using webhook: %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.ParentNotFound() {
|
||||||
|
msg.ParentID = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post normal message.
|
||||||
var msgReplaceID string
|
var msgReplaceID string
|
||||||
msgID := xid.New().String()
|
msgID := xid.New().String()
|
||||||
if msg.ID != "" {
|
if msg.ID != "" {
|
||||||
msgID = msg.ID
|
|
||||||
msgReplaceID = msg.ID
|
msgReplaceID = msg.ID
|
||||||
}
|
}
|
||||||
// Post normal message.
|
var replyID string
|
||||||
|
if res, ok := b.StanzaIDs.Get(msg.ParentID); ok {
|
||||||
|
replyID, _ = res.(string)
|
||||||
|
}
|
||||||
b.Log.Debugf("=> Sending message %#v", msg)
|
b.Log.Debugf("=> Sending message %#v", msg)
|
||||||
if _, err := b.xc.Send(xmpp.Chat{
|
if _, err := b.xc.Send(xmpp.Chat{
|
||||||
Type: "groupchat",
|
Type: "groupchat",
|
||||||
@ -116,18 +153,53 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
|
|||||||
Text: msg.Username + msg.Text,
|
Text: msg.Username + msg.Text,
|
||||||
ID: msgID,
|
ID: msgID,
|
||||||
ReplaceID: msgReplaceID,
|
ReplaceID: msgReplaceID,
|
||||||
|
ReplyID: replyID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return msgID, nil
|
return msgID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bxmpp) createXMPP() error {
|
func (b *Bxmpp) postSlackCompatibleWebhook(msg config.Message) error {
|
||||||
if !strings.Contains(b.GetString("Jid"), "@") {
|
type XMPPWebhook struct {
|
||||||
return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid"))
|
Username string `json:"username"`
|
||||||
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
webhookBody, err := json.Marshal(XMPPWebhook{
|
||||||
|
Username: msg.Username,
|
||||||
|
Text: msg.Text,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Failed to marshal webhook: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Post(b.GetString("WebhookURL")+"/"+url.QueryEscape(msg.Channel), "application/json", bytes.NewReader(webhookBody))
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("Failed to POST webhook: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bxmpp) createXMPP() error {
|
||||||
|
var serverName string
|
||||||
|
switch {
|
||||||
|
case !b.GetBool("Anonymous"):
|
||||||
|
if !strings.Contains(b.GetString("Jid"), "@") {
|
||||||
|
return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid"))
|
||||||
|
}
|
||||||
|
serverName = strings.Split(b.GetString("Jid"), "@")[1]
|
||||||
|
case !strings.Contains(b.GetString("Server"), ":"):
|
||||||
|
serverName = strings.Split(b.GetString("Server"), ":")[0]
|
||||||
|
default:
|
||||||
|
serverName = b.GetString("Server")
|
||||||
|
}
|
||||||
|
|
||||||
tc := &tls.Config{
|
tc := &tls.Config{
|
||||||
ServerName: strings.Split(b.GetString("Jid"), "@")[1],
|
ServerName: serverName,
|
||||||
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec
|
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +300,13 @@ func (b *Bxmpp) handleXMPP() error {
|
|||||||
for {
|
for {
|
||||||
m, err := b.xc.Recv()
|
m, err := b.xc.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
// An error together with AvatarData is non-fatal
|
||||||
|
switch m.(type) {
|
||||||
|
case xmpp.AvatarData:
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v := m.(type) {
|
switch v := m.(type) {
|
||||||
@ -236,6 +314,11 @@ func (b *Bxmpp) handleXMPP() error {
|
|||||||
if v.Type == "groupchat" {
|
if v.Type == "groupchat" {
|
||||||
b.Log.Debugf("== Receiving %#v", v)
|
b.Log.Debugf("== Receiving %#v", v)
|
||||||
|
|
||||||
|
if v.ID != "" && v.StanzaID != "" {
|
||||||
|
b.StanzaIDs.Add(v.ID, v.StanzaID)
|
||||||
|
b.OriginIDs.Add(v.StanzaID, v.ID)
|
||||||
|
}
|
||||||
|
|
||||||
// Skip invalid messages.
|
// Skip invalid messages.
|
||||||
if b.skipMessage(v) {
|
if b.skipMessage(v) {
|
||||||
continue
|
continue
|
||||||
@ -260,6 +343,12 @@ func (b *Bxmpp) handleXMPP() error {
|
|||||||
if v.ReplaceID != "" {
|
if v.ReplaceID != "" {
|
||||||
msgID = v.ReplaceID
|
msgID = v.ReplaceID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var parentID string
|
||||||
|
if res, ok := b.OriginIDs.Get(v.ReplyID); ok {
|
||||||
|
parentID, _ = res.(string)
|
||||||
|
}
|
||||||
|
|
||||||
rmsg := config.Message{
|
rmsg := config.Message{
|
||||||
Username: b.parseNick(v.Remote),
|
Username: b.parseNick(v.Remote),
|
||||||
Text: v.Text,
|
Text: v.Text,
|
||||||
@ -267,6 +356,7 @@ func (b *Bxmpp) handleXMPP() error {
|
|||||||
Account: b.Account,
|
Account: b.Account,
|
||||||
Avatar: avatar,
|
Avatar: avatar,
|
||||||
UserID: v.Remote,
|
UserID: v.Remote,
|
||||||
|
ParentID: parentID,
|
||||||
ID: msgID,
|
ID: msgID,
|
||||||
Event: event,
|
Event: event,
|
||||||
}
|
}
|
||||||
@ -339,7 +429,7 @@ func (b *Bxmpp) handleUploadFile(msg *config.Message) error {
|
|||||||
|
|
||||||
func (b *Bxmpp) parseNick(remote string) string {
|
func (b *Bxmpp) parseNick(remote string) string {
|
||||||
s := strings.Split(remote, "@")
|
s := strings.Split(remote, "@")
|
||||||
if len(s) > 0 {
|
if len(s) > 1 {
|
||||||
s = strings.Split(s[1], "/")
|
s = strings.Split(s[1], "/")
|
||||||
if len(s) == 2 {
|
if len(s) == 2 {
|
||||||
return s[1] // nick
|
return s[1] // nick
|
||||||
@ -378,6 +468,11 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore messages posted by our webhook
|
||||||
|
if b.GetString("WebhookURL") != "" && strings.Contains(message.ID, "webhookbot") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// skip delayed messages
|
// skip delayed messages
|
||||||
return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5
|
return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package bzulip
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/42wim/matterbridge/bridge"
|
"github.com/42wim/matterbridge/bridge"
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/bridge/helper"
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
|
"github.com/42wim/matterbridge/version"
|
||||||
gzb "github.com/matterbridge/gozulipbot"
|
gzb "github.com/matterbridge/gozulipbot"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ func New(cfg *bridge.Config) bridge.Bridger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bzulip) Connect() error {
|
func (b *Bzulip) Connect() error {
|
||||||
bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login")}
|
bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login"), UserAgent: fmt.Sprintf("matterbridge/%s", version.Release)}
|
||||||
bot.Init()
|
bot.Init()
|
||||||
q, err := bot.RegisterAll()
|
q, err := bot.RegisterAll()
|
||||||
b.q = q
|
b.q = q
|
||||||
@ -104,29 +106,31 @@ func (b *Bzulip) getChannel(id int) string {
|
|||||||
func (b *Bzulip) handleQueue() error {
|
func (b *Bzulip) handleQueue() error {
|
||||||
for {
|
for {
|
||||||
messages, err := b.q.GetEvents()
|
messages, err := b.q.GetEvents()
|
||||||
switch err {
|
|
||||||
case gzb.BackoffError:
|
|
||||||
time.Sleep(time.Second * 5)
|
|
||||||
case gzb.NoJSONError:
|
|
||||||
b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds")
|
|
||||||
time.Sleep(time.Second * 10)
|
|
||||||
case gzb.BadEventQueueError:
|
|
||||||
b.Log.Info("got a bad event queue id error, reconnecting")
|
|
||||||
b.bot.Queues = nil
|
|
||||||
for {
|
|
||||||
b.q, err = b.bot.RegisterAll()
|
|
||||||
if err != nil {
|
|
||||||
b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
|
|
||||||
time.Sleep(time.Second * 10)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case gzb.HeartbeatError:
|
|
||||||
b.Log.Debug("heartbeat received.")
|
|
||||||
default:
|
|
||||||
b.Log.Debugf("receiving error: %#v", err)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case gzb.BackoffError:
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
case gzb.NoJSONError:
|
||||||
|
b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds")
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
|
case gzb.BadEventQueueError:
|
||||||
|
b.Log.Info("got a bad event queue id error, reconnecting")
|
||||||
|
b.bot.Queues = nil
|
||||||
|
for {
|
||||||
|
b.q, err = b.bot.RegisterAll()
|
||||||
|
if err != nil {
|
||||||
|
b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case gzb.HeartbeatError:
|
||||||
|
b.Log.Debug("heartbeat received.")
|
||||||
|
default:
|
||||||
|
b.Log.Debugf("receiving error: %#v", err)
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
|
1538
changelog.md
15
contrib/inmessage-logger.tengo
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
fmt := import("fmt")
|
||||||
|
os := import("os")
|
||||||
|
times := import("times")
|
||||||
|
|
||||||
|
if msgText != "" && msgUsername != "system" {
|
||||||
|
os.chdir("/var/www/matterbridge")
|
||||||
|
file := os.open_file("inmessage.log", os.o_append|os.o_wronly|os.o_create, 0644)
|
||||||
|
file.write_string(fmt.sprintf(
|
||||||
|
"[%s] <%s> %s\n",
|
||||||
|
times.time_format(times.now(), times.format_rfc1123),
|
||||||
|
msgUsername,
|
||||||
|
msgText
|
||||||
|
))
|
||||||
|
file.close()
|
||||||
|
}
|
19
contrib/matterbridge.openrc
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
#!/sbin/openrc-run
|
||||||
|
# Copyright 2021-2022 Gentoo Authors
|
||||||
|
# Distributed under the terms of the GNU General Public License v2
|
||||||
|
|
||||||
|
command=/usr/bin/matterbridge
|
||||||
|
command_args="-conf ${MATTERBRIDGE_CONF:-/etc/matterbridge/bridge.toml} ${MATTERBRIDGE_ARGS}"
|
||||||
|
command_user="matterbridge:matterbridge"
|
||||||
|
pidfile="/run/${RC_SVCNAME}.pid"
|
||||||
|
command_background=1
|
||||||
|
output_log="/var/log/${RC_SVCNAME}.log"
|
||||||
|
error_log="${output_log}"
|
||||||
|
|
||||||
|
depend() {
|
||||||
|
need net
|
||||||
|
}
|
||||||
|
|
||||||
|
start_pre() {
|
||||||
|
checkpath -f "${output_log}" -o "${command_user}" || return 1
|
||||||
|
}
|
6
contrib/mumble-autolink.tengo
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
text := import("text")
|
||||||
|
|
||||||
|
if outProtocol == "mumble" {
|
||||||
|
urlRE := text.re_compile(`(?is)((http|https):\/\/)?([a-z0-9-]+\.)?[a-z0-9-]+(\.[a-z]{2,6}){1,3}(\/[a-z0-9.,_\/~#&=;%+?-]*)?`)
|
||||||
|
msgText = urlRE.replace(msgText,`<a href="$0">$0</a>`)
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
FROM alpine:edge as certs
|
FROM alpine:edge as certs
|
||||||
RUN apk --update add ca-certificates
|
RUN apk --update add ca-certificates
|
||||||
|
ARG VERSION=1.22.3
|
||||||
|
ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-${VERSION}-linux-arm64 /bin/matterbridge
|
||||||
|
RUN chmod +x /bin/matterbridge
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
ARG VERSION=1.12.3
|
|
||||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-linux-arm /bin/matterbridge
|
COPY --from=certs /bin/matterbridge /bin/matterbridge
|
||||||
RUN chmod +x /bin/matterbridge
|
|
||||||
ENTRYPOINT ["/bin/matterbridge"]
|
ENTRYPOINT ["/bin/matterbridge"]
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
// +build !nogitter
|
|
||||||
|
|
||||||
package bridgemap
|
|
||||||
|
|
||||||
import (
|
|
||||||
bgitter "github.com/42wim/matterbridge/bridge/gitter"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
FullMap["gitter"] = bgitter.New
|
|
||||||
}
|
|
12
gateway/bridgemap/bharmony.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
//go:build !noharmony
|
||||||
|
// +build !noharmony
|
||||||
|
|
||||||
|
package bridgemap
|
||||||
|
|
||||||
|
import (
|
||||||
|
bharmony "github.com/42wim/matterbridge/bridge/harmony"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
FullMap["harmony"] = bharmony.New
|
||||||
|
}
|
11
gateway/bridgemap/bvk.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// +build !novk
|
||||||
|
|
||||||
|
package bridgemap
|
||||||
|
|
||||||
|
import (
|
||||||
|
bvk "github.com/42wim/matterbridge/bridge/vk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
FullMap["vk"] = bvk.New
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
// +build !nowhatsapp
|
// +build !nowhatsapp
|
||||||
|
// +build !whatsappmulti
|
||||||
|
|
||||||
package bridgemap
|
package bridgemap
|
||||||
|
|
||||||
|
11
gateway/bridgemap/bwhatsappmulti.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// +build whatsappmulti
|
||||||
|
|
||||||
|
package bridgemap
|
||||||
|
|
||||||
|
import (
|
||||||
|
bwhatsapp "github.com/42wim/matterbridge/bridge/whatsappmulti"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
FullMap["whatsapp"] = bwhatsapp.New
|
||||||
|
}
|
@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/d5/tengo/v2"
|
"github.com/d5/tengo/v2"
|
||||||
"github.com/d5/tengo/v2/stdlib"
|
"github.com/d5/tengo/v2/stdlib"
|
||||||
lru "github.com/hashicorp/golang-lru"
|
lru "github.com/hashicorp/golang-lru"
|
||||||
"github.com/matterbridge/emoji"
|
"github.com/kyokomi/emoji/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway {
|
|||||||
func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
|
func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
|
||||||
ID := protocol + " " + mID
|
ID := protocol + " " + mID
|
||||||
if gw.Messages.Contains(ID) {
|
if gw.Messages.Contains(ID) {
|
||||||
return mID
|
return ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not keyed, iterate through cache for downstream, and infer upstream.
|
// If not keyed, iterate through cache for downstream, and infer upstream.
|
||||||
@ -75,7 +75,7 @@ func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
|
|||||||
ids := v.([]*BrMsgID)
|
ids := v.([]*BrMsgID)
|
||||||
for _, downstreamMsgObj := range ids {
|
for _, downstreamMsgObj := range ids {
|
||||||
if ID == downstreamMsgObj.ID {
|
if ID == downstreamMsgObj.ID {
|
||||||
return strings.Replace(mid.(string), protocol+" ", "", 1)
|
return mid.(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ func (gw *Gateway) AddConfig(cfg *config.Gateway) error {
|
|||||||
gw.logger.Errorf("mapChannels() failed: %s", err)
|
gw.logger.Errorf("mapChannels() failed: %s", err)
|
||||||
}
|
}
|
||||||
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
|
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
|
||||||
br := br //scopelint
|
br := br // scopelint
|
||||||
err := gw.AddBridge(&br)
|
err := gw.AddBridge(&br)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -299,13 +299,30 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
|
|||||||
|
|
||||||
igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks"))
|
igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks"))
|
||||||
igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages"))
|
igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages"))
|
||||||
if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) {
|
if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) || gw.ignoreFilesComment(msg.Extra, igMessages) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignoreFilesComment returns true if we need to ignore a file with matched comment.
|
||||||
|
func (gw *Gateway) ignoreFilesComment(extra map[string][]interface{}, igMessages []string) bool {
|
||||||
|
if extra == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, f := range extra["file"] {
|
||||||
|
fi, ok := f.(config.FileInfo)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if gw.ignoreText(fi.Comment, igMessages) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string {
|
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string {
|
||||||
if dest.GetBool("StripNick") {
|
if dest.GetBool("StripNick") {
|
||||||
re := regexp.MustCompile("[^a-zA-Z0-9]+")
|
re := regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||||
@ -337,20 +354,21 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri
|
|||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
|
nick = strings.ReplaceAll(nick, "{NOPINGNICK}", msg.Username[:i]+"\u200b"+msg.Username[i:])
|
||||||
}
|
}
|
||||||
|
|
||||||
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
|
nick = strings.ReplaceAll(nick, "{BRIDGE}", br.Name)
|
||||||
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
|
nick = strings.ReplaceAll(nick, "{PROTOCOL}", br.Protocol)
|
||||||
nick = strings.Replace(nick, "{GATEWAY}", gw.Name, -1)
|
nick = strings.ReplaceAll(nick, "{GATEWAY}", gw.Name)
|
||||||
nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1)
|
nick = strings.ReplaceAll(nick, "{LABEL}", br.GetString("Label"))
|
||||||
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
|
nick = strings.ReplaceAll(nick, "{NICK}", msg.Username)
|
||||||
nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1)
|
nick = strings.ReplaceAll(nick, "{USERID}", msg.UserID)
|
||||||
|
nick = strings.ReplaceAll(nick, "{CHANNEL}", msg.Channel)
|
||||||
tengoNick, err := gw.modifyUsernameTengo(msg, br)
|
tengoNick, err := gw.modifyUsernameTengo(msg, br)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gw.logger.Errorf("modifyUsernameTengo error: %s", err)
|
gw.logger.Errorf("modifyUsernameTengo error: %s", err)
|
||||||
}
|
}
|
||||||
nick = strings.Replace(nick, "{TENGO}", tengoNick, -1) //nolint:gocritic
|
nick = strings.ReplaceAll(nick, "{TENGO}", tengoNick)
|
||||||
return nick
|
return nick
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,6 +403,7 @@ func (gw *Gateway) modifyMessage(msg *config.Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// replace :emoji: to unicode
|
// replace :emoji: to unicode
|
||||||
|
emoji.ReplacePadding = ""
|
||||||
msg.Text = emoji.Sprint(msg.Text)
|
msg.Text = emoji.Sprint(msg.Text)
|
||||||
|
|
||||||
br := gw.Bridges[msg.Account]
|
br := gw.Bridges[msg.Account]
|
||||||
@ -445,22 +464,25 @@ func (gw *Gateway) SendMessage(
|
|||||||
msg.Avatar = gw.modifyAvatar(rmsg, dest)
|
msg.Avatar = gw.modifyAvatar(rmsg, dest)
|
||||||
msg.Username = gw.modifyUsername(rmsg, dest)
|
msg.Username = gw.modifyUsername(rmsg, dest)
|
||||||
|
|
||||||
msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel)
|
// exclude file delete event as the msg ID here is the native file ID that needs to be deleted
|
||||||
|
if msg.Event != config.EventFileDelete {
|
||||||
|
msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel)
|
||||||
|
}
|
||||||
|
|
||||||
// for api we need originchannel as channel
|
// for api we need originchannel as channel
|
||||||
if dest.Protocol == apiProtocol {
|
if dest.Protocol == apiProtocol {
|
||||||
msg.Channel = rmsg.Channel
|
msg.Channel = rmsg.Channel
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel)
|
msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel)
|
||||||
if msg.ParentID == "" {
|
if msg.ParentID == "" {
|
||||||
msg.ParentID = canonicalParentMsgID
|
msg.ParentID = strings.Replace(canonicalParentMsgID, dest.Protocol+" ", "", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the parentID is still empty and we have a parentID set in the original message
|
// if the parentID is still empty and we have a parentID set in the original message
|
||||||
// this means that we didn't find it in the cache so set it "msg-parent-not-found"
|
// this means that we didn't find it in the cache so set it to a "msg-parent-not-found" constant
|
||||||
if msg.ParentID == "" && rmsg.ParentID != "" {
|
if msg.ParentID == "" && rmsg.ParentID != "" {
|
||||||
msg.ParentID = "msg-parent-not-found"
|
msg.ParentID = config.ParentIDNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
drop, err := gw.modifyOutMessageTengo(rmsg, &msg, dest)
|
drop, err := gw.modifyOutMessageTengo(rmsg, &msg, dest)
|
||||||
@ -495,7 +517,7 @@ func (gw *Gateway) SendMessage(
|
|||||||
if mID != "" {
|
if mID != "" {
|
||||||
gw.logger.Debugf("mID %s: %s", dest.Account, mID)
|
gw.logger.Debugf("mID %s: %s", dest.Account, mID)
|
||||||
return mID, nil
|
return mID, nil
|
||||||
//brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID})
|
// brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID})
|
||||||
}
|
}
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
@ -549,6 +571,7 @@ func modifyInMessageTengo(filename string, msg *config.Message) error {
|
|||||||
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
|
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
|
||||||
_ = s.Add("msgText", msg.Text)
|
_ = s.Add("msgText", msg.Text)
|
||||||
_ = s.Add("msgUsername", msg.Username)
|
_ = s.Add("msgUsername", msg.Username)
|
||||||
|
_ = s.Add("msgUserID", msg.UserID)
|
||||||
_ = s.Add("msgAccount", msg.Account)
|
_ = s.Add("msgAccount", msg.Account)
|
||||||
_ = s.Add("msgChannel", msg.Channel)
|
_ = s.Add("msgChannel", msg.Channel)
|
||||||
c, err := s.Compile()
|
c, err := s.Compile()
|
||||||
@ -577,6 +600,7 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) (
|
|||||||
_ = s.Add("result", "")
|
_ = s.Add("result", "")
|
||||||
_ = s.Add("msgText", msg.Text)
|
_ = s.Add("msgText", msg.Text)
|
||||||
_ = s.Add("msgUsername", msg.Username)
|
_ = s.Add("msgUsername", msg.Username)
|
||||||
|
_ = s.Add("msgUserID", msg.UserID)
|
||||||
_ = s.Add("nick", msg.Username)
|
_ = s.Add("nick", msg.Username)
|
||||||
_ = s.Add("msgAccount", msg.Account)
|
_ = s.Add("msgAccount", msg.Account)
|
||||||
_ = s.Add("msgChannel", msg.Channel)
|
_ = s.Add("msgChannel", msg.Channel)
|
||||||
@ -631,6 +655,7 @@ func (gw *Gateway) modifyOutMessageTengo(origmsg *config.Message, msg *config.Me
|
|||||||
_ = s.Add("outEvent", msg.Event)
|
_ = s.Add("outEvent", msg.Event)
|
||||||
_ = s.Add("msgText", msg.Text)
|
_ = s.Add("msgText", msg.Text)
|
||||||
_ = s.Add("msgUsername", msg.Username)
|
_ = s.Add("msgUsername", msg.Username)
|
||||||
|
_ = s.Add("msgUserID", msg.UserID)
|
||||||
_ = s.Add("msgDrop", drop)
|
_ = s.Add("msgDrop", drop)
|
||||||
c, err := s.Compile()
|
c, err := s.Compile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -18,8 +18,6 @@ var testconfig = []byte(`
|
|||||||
server=""
|
server=""
|
||||||
[mattermost.test]
|
[mattermost.test]
|
||||||
server=""
|
server=""
|
||||||
[gitter.42wim]
|
|
||||||
server=""
|
|
||||||
[discord.test]
|
[discord.test]
|
||||||
server=""
|
server=""
|
||||||
[slack.test]
|
[slack.test]
|
||||||
@ -33,11 +31,6 @@ server=""
|
|||||||
account = "irc.freenode"
|
account = "irc.freenode"
|
||||||
channel = "#wimtesting"
|
channel = "#wimtesting"
|
||||||
|
|
||||||
[[gateway.inout]]
|
|
||||||
account="gitter.42wim"
|
|
||||||
channel="42wim/testroom"
|
|
||||||
#channel="matterbridge/Lobby"
|
|
||||||
|
|
||||||
[[gateway.inout]]
|
[[gateway.inout]]
|
||||||
account = "discord.test"
|
account = "discord.test"
|
||||||
channel = "general"
|
channel = "general"
|
||||||
@ -52,8 +45,6 @@ var testconfig2 = []byte(`
|
|||||||
server=""
|
server=""
|
||||||
[mattermost.test]
|
[mattermost.test]
|
||||||
server=""
|
server=""
|
||||||
[gitter.42wim]
|
|
||||||
server=""
|
|
||||||
[discord.test]
|
[discord.test]
|
||||||
server=""
|
server=""
|
||||||
[slack.test]
|
[slack.test]
|
||||||
@ -67,10 +58,6 @@ server=""
|
|||||||
account = "irc.freenode"
|
account = "irc.freenode"
|
||||||
channel = "#wimtesting"
|
channel = "#wimtesting"
|
||||||
|
|
||||||
[[gateway.in]]
|
|
||||||
account="gitter.42wim"
|
|
||||||
channel="42wim/testroom"
|
|
||||||
|
|
||||||
[[gateway.inout]]
|
[[gateway.inout]]
|
||||||
account = "discord.test"
|
account = "discord.test"
|
||||||
channel = "general"
|
channel = "general"
|
||||||
@ -86,10 +73,6 @@ server=""
|
|||||||
account = "irc.freenode"
|
account = "irc.freenode"
|
||||||
channel = "#wimtesting2"
|
channel = "#wimtesting2"
|
||||||
|
|
||||||
[[gateway.out]]
|
|
||||||
account="gitter.42wim"
|
|
||||||
channel="42wim/testroom"
|
|
||||||
|
|
||||||
[[gateway.out]]
|
[[gateway.out]]
|
||||||
account = "discord.test"
|
account = "discord.test"
|
||||||
channel = "general2"
|
channel = "general2"
|
||||||
@ -184,31 +167,18 @@ func maketestRouter(input []byte) *Router {
|
|||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewRouter(t *testing.T) {
|
func TestNewRouter(t *testing.T) {
|
||||||
r := maketestRouter(testconfig)
|
r := maketestRouter(testconfig)
|
||||||
assert.Equal(t, 1, len(r.Gateways))
|
assert.Equal(t, 1, len(r.Gateways))
|
||||||
assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
|
assert.Equal(t, 3, len(r.Gateways["bridge1"].Bridges))
|
||||||
assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
|
assert.Equal(t, 3, len(r.Gateways["bridge1"].Channels))
|
||||||
r = maketestRouter(testconfig2)
|
r = maketestRouter(testconfig2)
|
||||||
assert.Equal(t, 2, len(r.Gateways))
|
assert.Equal(t, 2, len(r.Gateways))
|
||||||
assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
|
assert.Equal(t, 3, len(r.Gateways["bridge1"].Bridges))
|
||||||
assert.Equal(t, 3, len(r.Gateways["bridge2"].Bridges))
|
assert.Equal(t, 2, len(r.Gateways["bridge2"].Bridges))
|
||||||
assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
|
assert.Equal(t, 3, len(r.Gateways["bridge1"].Channels))
|
||||||
assert.Equal(t, 3, len(r.Gateways["bridge2"].Channels))
|
assert.Equal(t, 2, len(r.Gateways["bridge2"].Channels))
|
||||||
assert.Equal(t, &config.ChannelInfo{
|
|
||||||
Name: "42wim/testroom",
|
|
||||||
Direction: "out",
|
|
||||||
ID: "42wim/testroomgitter.42wim",
|
|
||||||
Account: "gitter.42wim",
|
|
||||||
SameChannel: map[string]bool{"bridge2": false},
|
|
||||||
}, r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"])
|
|
||||||
assert.Equal(t, &config.ChannelInfo{
|
|
||||||
Name: "42wim/testroom",
|
|
||||||
Direction: "in",
|
|
||||||
ID: "42wim/testroomgitter.42wim",
|
|
||||||
Account: "gitter.42wim",
|
|
||||||
SameChannel: map[string]bool{"bridge1": false},
|
|
||||||
}, r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"])
|
|
||||||
assert.Equal(t, &config.ChannelInfo{
|
assert.Equal(t, &config.ChannelInfo{
|
||||||
Name: "general",
|
Name: "general",
|
||||||
Direction: "inout",
|
Direction: "inout",
|
||||||
@ -241,8 +211,6 @@ func TestGetDestChannel(t *testing.T) {
|
|||||||
SameChannel: map[string]bool{"bridge1": false},
|
SameChannel: map[string]bool{"bridge1": false},
|
||||||
Options: config.ChannelOptions{Key: ""},
|
Options: config.ChannelOptions{Key: ""},
|
||||||
}}, r.Gateways["bridge1"].getDestChannel(msg, *br))
|
}}, r.Gateways["bridge1"].getDestChannel(msg, *br))
|
||||||
case "gitter.42wim":
|
|
||||||
assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
|
|
||||||
case "irc.freenode":
|
case "irc.freenode":
|
||||||
assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
|
assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
|
||||||
}
|
}
|
||||||
@ -420,6 +388,7 @@ func (s *ignoreTestSuite) SetupSuite() {
|
|||||||
logger.SetOutput(ioutil.Discard)
|
logger.SetOutput(ioutil.Discard)
|
||||||
s.gw = &Gateway{logger: logrus.NewEntry(logger)}
|
s.gw = &Gateway{logger: logrus.NewEntry(logger)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ignoreTestSuite) TestIgnoreTextEmpty() {
|
func (s *ignoreTestSuite) TestIgnoreTextEmpty() {
|
||||||
extraFile := make(map[string][]interface{})
|
extraFile := make(map[string][]interface{})
|
||||||
extraAttach := make(map[string][]interface{})
|
extraAttach := make(map[string][]interface{})
|
||||||
@ -461,7 +430,6 @@ func (s *ignoreTestSuite) TestIgnoreTextEmpty() {
|
|||||||
output := s.gw.ignoreTextEmpty(testcase.input)
|
output := s.gw.ignoreTextEmpty(testcase.input)
|
||||||
s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)
|
s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ignoreTestSuite) TestIgnoreTexts() {
|
func (s *ignoreTestSuite) TestIgnoreTexts() {
|
||||||
|
@ -110,7 +110,9 @@ func (r *Router) disableBridge(br *bridge.Bridge, err error) bool {
|
|||||||
if r.BridgeValues().General.IgnoreFailureOnStart {
|
if r.BridgeValues().General.IgnoreFailureOnStart {
|
||||||
r.logger.Error(err)
|
r.logger.Error(err)
|
||||||
// setting this bridge empty
|
// setting this bridge empty
|
||||||
*br = bridge.Bridge{}
|
*br = bridge.Bridge{
|
||||||
|
Log: br.Log,
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -160,7 +162,7 @@ func (r *Router) handleReceive() {
|
|||||||
// For some bridges we always add/update the message ID.
|
// For some bridges we always add/update the message ID.
|
||||||
// This is necessary as msgIDs will change if a bridge returns
|
// This is necessary as msgIDs will change if a bridge returns
|
||||||
// a different ID in response to edits.
|
// a different ID in response to edits.
|
||||||
if !exists || msg.Protocol == "discord" {
|
if !exists {
|
||||||
gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs)
|
gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
185
go.mod
@ -1,58 +1,153 @@
|
|||||||
module github.com/42wim/matterbridge
|
module github.com/42wim/matterbridge
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
|
|
||||||
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
|
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
|
||||||
github.com/Jeffail/gabs v1.1.1 // indirect
|
github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f
|
||||||
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560
|
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560
|
||||||
github.com/Rhymen/go-whatsapp v0.1.2-0.20201122130733-6e5488ac98df
|
github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c
|
||||||
github.com/d5/tengo/v2 v2.6.2
|
github.com/SevereCloud/vksdk/v2 v2.17.0
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/bwmarrin/discordgo v0.28.1
|
||||||
github.com/fsnotify/fsnotify v1.4.9
|
github.com/d5/tengo/v2 v2.17.0
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api v1.0.1-0.20200524105306-7434b0456e81
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||||
github.com/gomarkdown/markdown v0.0.0-20201113031856-722100d81a8e
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
github.com/google/gops v0.3.13
|
github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2
|
||||||
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect
|
github.com/google/gops v0.3.27
|
||||||
github.com/gorilla/schema v1.2.0
|
github.com/gorilla/schema v1.4.1
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/harmony-development/shibshib v0.0.0-20220101224523-c98059d09cfa
|
||||||
github.com/hashicorp/golang-lru v0.5.4
|
github.com/hashicorp/golang-lru v1.0.2
|
||||||
github.com/jpillora/backoff v1.0.0
|
github.com/jpillora/backoff v1.0.0
|
||||||
github.com/keybase/go-keybase-chat-bot v0.0.0-20200505163032-5cacf52379da
|
github.com/keybase/go-keybase-chat-bot v0.0.0-20221220212439-e48d9abd2c20
|
||||||
github.com/labstack/echo/v4 v4.1.17
|
github.com/kyokomi/emoji/v2 v2.2.13
|
||||||
github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd
|
github.com/lrstanley/girc v0.0.0-20240823210506-80555f2adb03
|
||||||
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20200411204219-d5c18ce75048
|
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696
|
||||||
github.com/matterbridge/discordgo v0.22.1
|
github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8
|
||||||
github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible
|
github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27
|
||||||
github.com/matterbridge/go-xmpp v0.0.0-20200418225040-c8a3a57b4050
|
github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75
|
||||||
github.com/matterbridge/gozulipbot v0.0.0-20200820220548-be5824faa913
|
|
||||||
github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba
|
github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba
|
||||||
github.com/mattermost/mattermost-server/v5 v5.29.0
|
github.com/matterbridge/matterclient v0.0.0-20240817214420-3d4c3aef3dc1
|
||||||
github.com/mattn/godown v0.0.0-20201027140031-2c7783b24de7
|
github.com/matterbridge/telegram-bot-api/v6 v6.5.0
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
github.com/mattermost/mattermost/server/public v0.1.6
|
||||||
github.com/missdeer/golib v1.0.4
|
github.com/mattn/godown v0.0.1
|
||||||
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect
|
github.com/mdp/qrterminal v1.0.1
|
||||||
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9
|
github.com/nelsonken/gomf v0.0.0-20190423072027-c65cc0469e94
|
||||||
|
github.com/olahol/melody v1.2.1
|
||||||
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c
|
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c
|
||||||
github.com/rs/xid v1.2.1
|
github.com/rs/xid v1.5.0
|
||||||
github.com/russross/blackfriday v1.5.2
|
github.com/russross/blackfriday v1.6.0
|
||||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
|
||||||
github.com/shazow/ssh-chat v1.10.1
|
github.com/shazow/ssh-chat v1.10.1
|
||||||
github.com/sirupsen/logrus v1.7.0
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/slack-go/slack v0.7.2
|
github.com/slack-go/slack v0.14.0
|
||||||
github.com/spf13/viper v1.7.1
|
github.com/spf13/viper v1.19.0
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50
|
github.com/vincent-petithory/dataurl v1.0.0
|
||||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
|
|
||||||
github.com/yaegashi/msgraph.go v0.1.4
|
github.com/yaegashi/msgraph.go v0.1.4
|
||||||
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2
|
github.com/zfjagann/golang-ring v0.0.0-20220330170733-19bcea1b6289
|
||||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
|
go.mau.fi/whatsmeow v0.0.0-20240821142752-3d63c6fcc1a7
|
||||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58
|
golang.org/x/image v0.19.0
|
||||||
gomod.garykim.dev/nc-talk v0.1.5
|
golang.org/x/oauth2 v0.22.0
|
||||||
gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376
|
golang.org/x/text v0.17.0
|
||||||
layeh.com/gumble v0.0.0-20200818122324-146f9205029b
|
gomod.garykim.dev/nc-talk v0.3.0
|
||||||
|
google.golang.org/protobuf v1.34.2
|
||||||
|
layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14
|
||||||
|
modernc.org/sqlite v1.32.0
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.15
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989 // indirect
|
||||||
|
github.com/Jeffail/gabs v1.4.0 // indirect
|
||||||
|
github.com/apex/log v1.9.0 // indirect
|
||||||
|
github.com/av-elier/go-decimal-to-rational v0.0.0-20191127152832-89e6aad02ecf // indirect
|
||||||
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
|
||||||
|
github.com/fatih/color v1.17.0 // indirect
|
||||||
|
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gopackage/ddp v0.0.3 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/hashicorp/go-plugin v1.6.1 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||||
|
github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect
|
||||||
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
|
||||||
|
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
|
||||||
|
github.com/mattermost/logr/v2 v2.0.21 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
|
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||||
|
github.com/monaco-io/request v1.0.5 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/nxadm/tail v1.4.11 // indirect
|
||||||
|
github.com/oklog/run v1.1.0 // indirect
|
||||||
|
github.com/pborman/uuid v1.2.1 // indirect
|
||||||
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/rickb777/date v1.12.4 // indirect
|
||||||
|
github.com/rickb777/plural v1.2.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/rs/zerolog v1.33.0 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 // indirect
|
||||||
|
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 // indirect
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tinylib/msgp v1.2.0 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
|
github.com/wiggin77/merror v1.0.5 // indirect
|
||||||
|
github.com/wiggin77/srslog v1.0.1 // indirect
|
||||||
|
go.mau.fi/libsignal v0.1.1 // indirect
|
||||||
|
go.mau.fi/util v0.6.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
|
||||||
|
golang.org/x/net v0.27.0 // indirect
|
||||||
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
golang.org/x/term v0.22.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect
|
||||||
|
google.golang.org/grpc v1.65.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||||
|
modernc.org/libc v1.55.3 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.8.0 // indirect
|
||||||
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
|
modernc.org/token v1.1.0 // indirect
|
||||||
|
rsc.io/qr v0.2.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
//replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419
|
||||||
|
|
||||||
|
go 1.22.0
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message for rocketchat outgoing webhook.
|
// Message for rocketchat outgoing webhook.
|
||||||
@ -68,7 +69,6 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
msg := Message{}
|
msg := Message{}
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
log.Println(string(body))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
@ -89,7 +89,11 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
msg.ChannelName = "#" + msg.ChannelName
|
msg.ChannelName = "#" + msg.ChannelName
|
||||||
if c.Token != "" {
|
if c.Token != "" {
|
||||||
if msg.Token != c.Token {
|
if msg.Token != c.Token {
|
||||||
log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr)
|
if regexp.MustCompile(`[^a-zA-Z0-9]+`).MatchString(msg.Token) {
|
||||||
|
log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr)
|
||||||
|
} else {
|
||||||
|
log.Println("invalid token from " + r.RemoteAddr)
|
||||||
|
}
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 270 KiB |
Before Width: | Height: | Size: 170 KiB |
Before Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 62 KiB |
@ -10,15 +10,13 @@ import (
|
|||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/gateway"
|
"github.com/42wim/matterbridge/gateway"
|
||||||
"github.com/42wim/matterbridge/gateway/bridgemap"
|
"github.com/42wim/matterbridge/gateway/bridgemap"
|
||||||
|
"github.com/42wim/matterbridge/version"
|
||||||
"github.com/google/gops/agent"
|
"github.com/google/gops/agent"
|
||||||
prefixed "github.com/matterbridge/logrus-prefixed-formatter"
|
prefixed "github.com/matterbridge/logrus-prefixed-formatter"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version = "1.20.1-dev"
|
|
||||||
githash string
|
|
||||||
|
|
||||||
flagConfig = flag.String("conf", "matterbridge.toml", "config file")
|
flagConfig = flag.String("conf", "matterbridge.toml", "config file")
|
||||||
flagDebug = flag.Bool("debug", false, "enable debug")
|
flagDebug = flag.Bool("debug", false, "enable debug")
|
||||||
flagVersion = flag.Bool("version", false, "show version")
|
flagVersion = flag.Bool("version", false, "show version")
|
||||||
@ -28,7 +26,7 @@ var (
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *flagVersion {
|
if *flagVersion {
|
||||||
fmt.Printf("version: %s %s\n", version, githash)
|
fmt.Printf("version: %s %s\n", version.Release, version.GitHash)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,8 +41,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Printf("Running version %s %s", version, githash)
|
logger.Printf("Running version %s %s", version.Release, version.GitHash)
|
||||||
if strings.Contains(version, "-dev") {
|
if strings.Contains(version.Release, "-dev") {
|
||||||
logger.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
|
logger.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +65,7 @@ func main() {
|
|||||||
if err = r.Start(); err != nil {
|
if err = r.Start(); err != nil {
|
||||||
logger.Fatalf("Starting gateway failed: %s", err)
|
logger.Fatalf("Starting gateway failed: %s", err)
|
||||||
}
|
}
|
||||||
logger.Printf("Gateway(s) started succesfully. Now relaying messages")
|
logger.Printf("Gateway(s) started successfully. Now relaying messages")
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
61
matterbridge.toml.multi
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
#WARNING: as this file contains credentials, be sure to set correct file permissions
|
||||||
|
|
||||||
|
[irc]
|
||||||
|
[irc.foo]
|
||||||
|
Server="irc.myfooserver.com:6667"
|
||||||
|
Nick="matterbot"
|
||||||
|
|
||||||
|
# Can also connect to multiple different servers of the same protocol:
|
||||||
|
[irc]
|
||||||
|
[irc.bar]
|
||||||
|
Server="irc.mybarserver.com:6667"
|
||||||
|
Nick="matterbot"
|
||||||
|
|
||||||
|
[telegram]
|
||||||
|
[telegram.mytelegram]
|
||||||
|
Token="123456789:FillInYourTokenHereThatIsImportant"
|
||||||
|
|
||||||
|
[mattermost]
|
||||||
|
[mattermost.work]
|
||||||
|
#do not prefix it wit http:// or https://
|
||||||
|
Server="yourmattermostserver.domain"
|
||||||
|
Team="yourteam"
|
||||||
|
Login="yourlogin"
|
||||||
|
Password="yourpass"
|
||||||
|
PrefixMessagesWithNick=true
|
||||||
|
|
||||||
|
# Bridge 1: Copy all messages from all rooms to all rooms.
|
||||||
|
# This shows how you can have multiple rooms in a single bridge.
|
||||||
|
[[gateway]]
|
||||||
|
name="cats-are-cool"
|
||||||
|
enable=true
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="irc.foo"
|
||||||
|
channel="#cats-are-cool"
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="irc.bar"
|
||||||
|
channel="#cats-are-cool"
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="telegram.mytelegram"
|
||||||
|
channel="-1234567890123"
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="mattermost.work"
|
||||||
|
channel="cats-are-cool"
|
||||||
|
|
||||||
|
# Bridge 2: Copy some messages from some rooms to some rooms.
|
||||||
|
# This shows how you can have multiple bridges.
|
||||||
|
[[gateway]]
|
||||||
|
name="dog-announcements"
|
||||||
|
enable=true
|
||||||
|
[[gateway.in]]
|
||||||
|
account="irc.foo"
|
||||||
|
channel="#dog-announcements"
|
||||||
|
[[gateway.in]]
|
||||||
|
account="irc.bar"
|
||||||
|
channel="#dog-announcements"
|
||||||
|
[[gateway.out]]
|
||||||
|
account="telegram.mytelegram"
|
||||||
|
channel="-9876543219876"
|
||||||
|
[[gateway.out]]
|
||||||
|
account="mattermost.work"
|
||||||
|
channel="dog-announcements"
|
@ -9,12 +9,12 @@
|
|||||||
[irc]
|
[irc]
|
||||||
|
|
||||||
#You can configure multiple servers "[irc.name]" or "[irc.name2]"
|
#You can configure multiple servers "[irc.name]" or "[irc.name2]"
|
||||||
#In this example we use [irc.freenode]
|
#In this example we use [irc.libera]
|
||||||
#REQUIRED
|
#REQUIRED
|
||||||
[irc.freenode]
|
[irc.libera]
|
||||||
#irc server to connect to.
|
#irc server to connect to.
|
||||||
#REQUIRED
|
#REQUIRED
|
||||||
Server="irc.freenode.net:6667"
|
Server="irc.libera.chat:6667"
|
||||||
|
|
||||||
#Password for irc server (if necessary)
|
#Password for irc server (if necessary)
|
||||||
#OPTIONAL (default "")
|
#OPTIONAL (default "")
|
||||||
@ -24,7 +24,14 @@ Password=""
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
UseTLS=false
|
UseTLS=false
|
||||||
|
|
||||||
#Enable SASL (PLAIN) authentication. (freenode requires this from eg AWS hosts)
|
#Use client certificate - see CertFP https://libera.chat/guides/certfp.html
|
||||||
|
#Specify filename which contains private key and cert
|
||||||
|
#OPTIONAL (default "")
|
||||||
|
#
|
||||||
|
#TLSClientCertificate="cert.pem"
|
||||||
|
TLSClientCertificate=""
|
||||||
|
|
||||||
|
#Enable SASL (PLAIN) authentication. (libera requires this from eg AWS hosts)
|
||||||
#It uses NickServNick and NickServPassword as login and password
|
#It uses NickServNick and NickServPassword as login and password
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
UseSASL=false
|
UseSASL=false
|
||||||
@ -34,6 +41,11 @@ UseSASL=false
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
SkipTLSVerify=true
|
SkipTLSVerify=true
|
||||||
|
|
||||||
|
#Local address to use for server connection
|
||||||
|
#Note that Server and Bind must resolve to addresses of the same family.
|
||||||
|
#OPTIONAL (default "")
|
||||||
|
Bind=""
|
||||||
|
|
||||||
#If you know your charset, you can specify it manually.
|
#If you know your charset, you can specify it manually.
|
||||||
#Otherwise it tries to detect this automatically. Select one below
|
#Otherwise it tries to detect this automatically. Select one below
|
||||||
# "iso-8859-2:1987", "iso-8859-9:1989", "866", "latin9", "iso-8859-10:1992", "iso-ir-109", "hebrew",
|
# "iso-8859-2:1987", "iso-8859-9:1989", "866", "latin9", "iso-8859-10:1992", "iso-ir-109", "hebrew",
|
||||||
@ -55,7 +67,15 @@ Charset=""
|
|||||||
#REQUIRED
|
#REQUIRED
|
||||||
Nick="matterbot"
|
Nick="matterbot"
|
||||||
|
|
||||||
#If you registered your bot with a service like Nickserv on freenode.
|
#Real name/gecos displayed in e.g. /WHOIS and /WHO
|
||||||
|
#OPTIONAL (defaults to the nick)
|
||||||
|
RealName="Matterbridge instance on IRC"
|
||||||
|
|
||||||
|
#IRC username/ident preceding the hostname in hostmasks and /WHOIS
|
||||||
|
#OPTIONAL (defaults to the nick)
|
||||||
|
UserName="bridge"
|
||||||
|
|
||||||
|
#If you registered your bot with a service like Nickserv on libera.
|
||||||
#Also being used when UseSASL=true
|
#Also being used when UseSASL=true
|
||||||
#
|
#
|
||||||
#Note: if you want do to quakenet auth, set NickServNick="Q@CServe.quakenet.org"
|
#Note: if you want do to quakenet auth, set NickServNick="Q@CServe.quakenet.org"
|
||||||
@ -76,20 +96,24 @@ MessageDelay=1300
|
|||||||
|
|
||||||
#Maximum amount of messages to hold in queue. If queue is full
|
#Maximum amount of messages to hold in queue. If queue is full
|
||||||
#messages will be dropped.
|
#messages will be dropped.
|
||||||
#<message clipped> will be add to the message that fills the queue.
|
#<clipped message> will be add to the message that fills the queue.
|
||||||
#OPTIONAL (default 30)
|
#OPTIONAL (default 30)
|
||||||
MessageQueue=30
|
MessageQueue=30
|
||||||
|
|
||||||
#Maximum length of message sent to irc server. If it exceeds
|
#Maximum length of message sent to irc server. If it exceeds
|
||||||
#<message clipped> will be add to the message.
|
#<clipped message> will be add to the message.
|
||||||
#OPTIONAL (default 400)
|
#OPTIONAL (default 400)
|
||||||
MessageLength=400
|
MessageLength=400
|
||||||
|
|
||||||
#Split messages on MessageLength instead of showing the <message clipped>
|
#Split messages on MessageLength instead of showing the <clipped message>
|
||||||
#WARNING: this could lead to flooding
|
#WARNING: this could lead to flooding
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
MessageSplit=false
|
MessageSplit=false
|
||||||
|
|
||||||
|
#Message to show when a message is too big
|
||||||
|
#Default "<clipped message>"
|
||||||
|
MessageClipped="<clipped message>"
|
||||||
|
|
||||||
#Delay in seconds to rejoin a channel when kicked
|
#Delay in seconds to rejoin a channel when kicked
|
||||||
#OPTIONAL (default 0)
|
#OPTIONAL (default 0)
|
||||||
RejoinDelay=0
|
RejoinDelay=0
|
||||||
@ -98,10 +122,11 @@ RejoinDelay=0
|
|||||||
#Only works in IRC right now.
|
#Only works in IRC right now.
|
||||||
ColorNicks=false
|
ColorNicks=false
|
||||||
|
|
||||||
#RunCommands allows you to send RAW irc commands after connection
|
#RunCommands allows you to send RAW irc commands after connection.
|
||||||
|
#The string {BOTNICK} (case sensitive) will be replaced with the bot's current nickname.
|
||||||
#Array of strings
|
#Array of strings
|
||||||
#OPTIONAL (default empty)
|
#OPTIONAL (default empty)
|
||||||
RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"]
|
RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something", "MODE {BOTNICK} +B"]
|
||||||
|
|
||||||
#PingDelay specifies how long to wait to send a ping to the irc server.
|
#PingDelay specifies how long to wait to send a ping to the irc server.
|
||||||
#You can use s for second, m for minute
|
#You can use s for second, m for minute
|
||||||
@ -138,7 +163,7 @@ IgnoreMessages="^~~ badword"
|
|||||||
ReplaceMessages=[ ["cat","dog"] ]
|
ReplaceMessages=[ ["cat","dog"] ]
|
||||||
|
|
||||||
#nicks you want to replace.
|
#nicks you want to replace.
|
||||||
#see replacemessages for syntaxa
|
#see replacemessages for syntax
|
||||||
#optional (default empty)
|
#optional (default empty)
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
ReplaceNicks=[ ["user--","user"] ]
|
||||||
|
|
||||||
@ -163,7 +188,7 @@ Label=""
|
|||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
#Enable to show users joins/parts from other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
@ -173,7 +198,7 @@ ShowJoinPart=false
|
|||||||
VerboseJoinPart=false
|
VerboseJoinPart=false
|
||||||
|
|
||||||
#Do not send joins/parts to other bridges
|
#Do not send joins/parts to other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
NoSendJoinPart=false
|
NoSendJoinPart=false
|
||||||
|
|
||||||
@ -193,6 +218,19 @@ ShowTopicChange=false
|
|||||||
#OPTIONAL (default 0)
|
#OPTIONAL (default 0)
|
||||||
JoinDelay=0
|
JoinDelay=0
|
||||||
|
|
||||||
|
#Use the optional RELAYMSG extension for username spoofing on IRC.
|
||||||
|
#This requires an IRCd that supports the draft/relaymsg specification: currently this includes
|
||||||
|
#Oragono 2.4.0+ and InspIRCd 3 with the m_relaymsg contrib module.
|
||||||
|
#See https://github.com/42wim/matterbridge/issues/667#issuecomment-634214165 for more details.
|
||||||
|
#Spoofed nicks will use the configured RemoteNickFormat, replacing reserved IRC characters
|
||||||
|
#(!+%@&#$:'"?*,.) with a hyphen (-).
|
||||||
|
#On most configurations, the RemoteNickFormat must include a separator character such as "/".
|
||||||
|
#You should make sure that the settings here match your IRCd.
|
||||||
|
#This option overrides ColorNicks.
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
UseRelayMsg=false
|
||||||
|
#RemoteNickFormat="{NICK}/{PROTOCOL}"
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#XMPP section
|
#XMPP section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -206,12 +244,16 @@ JoinDelay=0
|
|||||||
#REQUIRED
|
#REQUIRED
|
||||||
Server="jabber.example.com:5222"
|
Server="jabber.example.com:5222"
|
||||||
|
|
||||||
|
#Use anonymous MUC login
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
Anonymous=false
|
||||||
|
|
||||||
#Jid
|
#Jid
|
||||||
#REQUIRED
|
#REQUIRED if Anonymous=false
|
||||||
Jid="user@example.com"
|
Jid="user@example.com"
|
||||||
|
|
||||||
#Password
|
#Password
|
||||||
#REQUIRED
|
#REQUIRED if Anonymous=false
|
||||||
Password="yourpass"
|
Password="yourpass"
|
||||||
|
|
||||||
#MUC
|
#MUC
|
||||||
@ -259,7 +301,7 @@ IgnoreMessages="^~~ badword"
|
|||||||
ReplaceMessages=[ ["cat","dog"] ]
|
ReplaceMessages=[ ["cat","dog"] ]
|
||||||
|
|
||||||
#Nicks you want to replace.
|
#Nicks you want to replace.
|
||||||
#See ReplaceMessages for syntaxA
|
#See ReplaceMessages for syntax
|
||||||
#OPTIONAL (default empty)
|
#OPTIONAL (default empty)
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
ReplaceNicks=[ ["user--","user"] ]
|
||||||
|
|
||||||
@ -283,7 +325,7 @@ Label=""
|
|||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
#Enable to show users joins/parts from other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
@ -297,6 +339,11 @@ StripNick=false
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowTopicChange=false
|
ShowTopicChange=false
|
||||||
|
|
||||||
|
#Enable sending messages using a webhook instead of regular MUC messages.
|
||||||
|
#Only works with a prosody server using mod_slack_webhook. Does not support editing.
|
||||||
|
#OPTIONAL (default "")
|
||||||
|
WebhookURL="https://yourdomain/prosody/msg/someid"
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#mattermost section
|
#mattermost section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -362,6 +409,10 @@ SkipTLSVerify=true
|
|||||||
## RELOADABLE SETTINGS
|
## RELOADABLE SETTINGS
|
||||||
## Settings below can be reloaded by editing the file
|
## Settings below can be reloaded by editing the file
|
||||||
|
|
||||||
|
# UseUserName shows the username instead of the server nickname
|
||||||
|
# OPTIONAL (default false)
|
||||||
|
UseUserName=false
|
||||||
|
|
||||||
#how to format the list of IRC nicks when displayed in mattermost.
|
#how to format the list of IRC nicks when displayed in mattermost.
|
||||||
#Possible options are "table" and "plain"
|
#Possible options are "table" and "plain"
|
||||||
#OPTIONAL (default plain)
|
#OPTIONAL (default plain)
|
||||||
@ -417,7 +468,7 @@ IgnoreMessages="^~~ badword"
|
|||||||
ReplaceMessages=[ ["cat","dog"] ]
|
ReplaceMessages=[ ["cat","dog"] ]
|
||||||
|
|
||||||
#nicks you want to replace.
|
#nicks you want to replace.
|
||||||
#see replacemessages for syntaxa
|
#see replacemessages for syntax
|
||||||
#optional (default empty)
|
#optional (default empty)
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
ReplaceNicks=[ ["user--","user"] ]
|
||||||
|
|
||||||
@ -441,12 +492,12 @@ Label=""
|
|||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
#Enable to show users joins/parts from other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
#Do not send joins/parts to other bridges
|
#Do not send joins/parts to other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
NoSendJoinPart=false
|
NoSendJoinPart=false
|
||||||
|
|
||||||
@ -462,86 +513,9 @@ ShowTopicChange=false
|
|||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#Gitter section
|
#Gitter section
|
||||||
#Best to make a dedicated gitter account for the bot.
|
#Gitter has been moved to matrix - see https://github.com/42wim/matterbridge/issues/1969 how to migrate
|
||||||
###################################################################
|
###################################################################
|
||||||
|
|
||||||
[gitter]
|
|
||||||
|
|
||||||
#You can configure multiple servers "[gitter.name]" or "[gitter.name2]"
|
|
||||||
#In this example we use [gitter.myproject]
|
|
||||||
#REQUIRED
|
|
||||||
[gitter.myproject]
|
|
||||||
#Token to connect with Gitter API
|
|
||||||
#You can get your token by going to https://developer.gitter.im/docs/welcome and SIGN IN
|
|
||||||
#REQUIRED
|
|
||||||
Token="Yourtokenhere"
|
|
||||||
|
|
||||||
## RELOADABLE SETTINGS
|
|
||||||
## Settings below can be reloaded by editing the file
|
|
||||||
|
|
||||||
#Nicks you want to ignore.
|
|
||||||
#Regular expressions supported
|
|
||||||
#Messages from those users will not be sent to other bridges.
|
|
||||||
#OPTIONAL
|
|
||||||
IgnoreNicks="ircspammer1 ircspammer2"
|
|
||||||
|
|
||||||
#Messages you want to ignore.
|
|
||||||
#Messages matching these regexp will be ignored and not sent to other bridges
|
|
||||||
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
|
||||||
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
|
||||||
IgnoreMessages="^~~ badword"
|
|
||||||
|
|
||||||
#messages you want to replace.
|
|
||||||
#it replaces outgoing messages from the bridge.
|
|
||||||
#so you need to place it by the sending bridge definition.
|
|
||||||
#regular expressions supported
|
|
||||||
#some examples:
|
|
||||||
#this replaces cat => dog and sleep => awake
|
|
||||||
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
|
|
||||||
#this replaces every number with number. 123 => numbernumbernumber
|
|
||||||
#replacemessages=[ ["[0-9]","number"] ]
|
|
||||||
#optional (default empty)
|
|
||||||
ReplaceMessages=[ ["cat","dog"] ]
|
|
||||||
|
|
||||||
#nicks you want to replace.
|
|
||||||
#see replacemessages for syntaxa
|
|
||||||
#optional (default empty)
|
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
|
||||||
|
|
||||||
#Extractnicks is used to for example rewrite messages from other relaybots
|
|
||||||
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
|
|
||||||
#some examples:
|
|
||||||
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
|
|
||||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
|
|
||||||
#you can use multiple entries for multiplebots
|
|
||||||
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
|
|
||||||
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
|
|
||||||
#OPTIONAL (default empty)
|
|
||||||
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
|
|
||||||
|
|
||||||
#extra label that can be used in the RemoteNickFormat
|
|
||||||
#optional (default empty)
|
|
||||||
Label=""
|
|
||||||
|
|
||||||
#RemoteNickFormat defines how remote users appear on this bridge
|
|
||||||
#See [general] config section for default options
|
|
||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
|
||||||
#OPTIONAL (default false)
|
|
||||||
ShowJoinPart=false
|
|
||||||
|
|
||||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
|
||||||
#It will strip other characters from the nick
|
|
||||||
#OPTIONAL (default false)
|
|
||||||
StripNick=false
|
|
||||||
|
|
||||||
#Enable to show topic changes from other bridges
|
|
||||||
#Only works hiding/show topic changes from slack bridge for now
|
|
||||||
#OPTIONAL (default false)
|
|
||||||
ShowTopicChange=false
|
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#
|
#
|
||||||
# Keybase
|
# Keybase
|
||||||
@ -612,7 +586,7 @@ IgnoreMessages="^~~ badword"
|
|||||||
ReplaceMessages=[ ["cat","dog"] ]
|
ReplaceMessages=[ ["cat","dog"] ]
|
||||||
|
|
||||||
#nicks you want to replace.
|
#nicks you want to replace.
|
||||||
#see replacemessages for syntaxa
|
#see replacemessages for syntax
|
||||||
#optional (default empty)
|
#optional (default empty)
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
ReplaceNicks=[ ["user--","user"] ]
|
||||||
|
|
||||||
@ -636,7 +610,7 @@ Label=""
|
|||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
#Enable to show users joins/parts from other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
@ -751,7 +725,7 @@ IgnoreMessages="^~~ badword"
|
|||||||
ReplaceMessages=[ ["cat","dog"] ]
|
ReplaceMessages=[ ["cat","dog"] ]
|
||||||
|
|
||||||
#nicks you want to replace.
|
#nicks you want to replace.
|
||||||
#see replacemessages for syntaxa
|
#see replacemessages for syntax
|
||||||
#optional (default empty)
|
#optional (default empty)
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
ReplaceNicks=[ ["user--","user"] ]
|
||||||
|
|
||||||
@ -775,12 +749,12 @@ Label=""
|
|||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
#Enable to show users joins/parts from other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
#Do not send joins/parts to other bridges
|
#Do not send joins/parts to other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
NoSendJoinPart=false
|
NoSendJoinPart=false
|
||||||
|
|
||||||
@ -808,6 +782,14 @@ PreserveThreading=false
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowUserTyping=false
|
ShowUserTyping=false
|
||||||
|
|
||||||
|
#Message to show when a message is too big
|
||||||
|
#Default "<clipped message>"
|
||||||
|
MessageClipped="<clipped message>"
|
||||||
|
|
||||||
|
#If enabled use the slack "Real Name" as username.
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
UseFullName=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#discord section
|
#discord section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -830,6 +812,14 @@ Server="yourservername"
|
|||||||
## All settings below can be reloaded by editing the file.
|
## All settings below can be reloaded by editing the file.
|
||||||
## They are also all optional.
|
## They are also all optional.
|
||||||
|
|
||||||
|
# AllowMention controls which mentions are allowed. If not specified, all mentions are allowed.
|
||||||
|
# Note that even when a mention is not allowed, it will still be displayed nicely and be clickable. It just prevents the ping/notification.
|
||||||
|
#
|
||||||
|
# "everyone" allows @everyone and @here mentions
|
||||||
|
# "roles" allows @role mentions
|
||||||
|
# "users" allows @user mentions
|
||||||
|
AllowMention=["everyone", "roles", "users"]
|
||||||
|
|
||||||
# ShowEmbeds shows the title, description and URL of embedded messages (sent by other bots)
|
# ShowEmbeds shows the title, description and URL of embedded messages (sent by other bots)
|
||||||
ShowEmbeds=false
|
ShowEmbeds=false
|
||||||
|
|
||||||
@ -846,10 +836,11 @@ UseUserName=false
|
|||||||
# UseDiscriminator appends the `#xxxx` discriminator when used with UseUserName
|
# UseDiscriminator appends the `#xxxx` discriminator when used with UseUserName
|
||||||
UseDiscriminator=false
|
UseDiscriminator=false
|
||||||
|
|
||||||
# WebhookURL sends messages in the style of puppets.
|
# AutoWebhooks automatically configures message sending in the style of puppets.
|
||||||
# This only works if you have one discord channel, if you have multiple discord channels you'll have to specify it in the gateway config
|
# This is an easier alternative to manually configuring "WebhookURL" for each gateway,
|
||||||
# Example: "https://discordapp.com/api/webhooks/1234/abcd_xyzw"
|
# as turning this on will automatically load or create webhooks for each channel.
|
||||||
WebhookURL=""
|
# This feature requires the "Manage Webhooks" permission (either globally or as per-channel).
|
||||||
|
AutoWebhooks=false
|
||||||
|
|
||||||
# EditDisable disables sending of edits to other bridges
|
# EditDisable disables sending of edits to other bridges
|
||||||
EditDisable=false
|
EditDisable=false
|
||||||
@ -934,6 +925,17 @@ ShowTopicChange=false
|
|||||||
# Supported from the following bridges: slack
|
# Supported from the following bridges: slack
|
||||||
SyncTopic=false
|
SyncTopic=false
|
||||||
|
|
||||||
|
# Message to show when a message is too big
|
||||||
|
# Default "<clipped message>"
|
||||||
|
MessageClipped="<clipped message>"
|
||||||
|
|
||||||
|
# Before clipping, try to split messages into at most this many parts. 0 is treated like 1.
|
||||||
|
# Be careful with large numbers, as this might cause flooding.
|
||||||
|
# Example: A maximum telegram message of 4096 bytes is received. This requires 3 Discord
|
||||||
|
# messages (each capped at a hardcoded 1950 bytes).
|
||||||
|
# Default 1
|
||||||
|
MessageSplitMaxCount=3
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#telegram section
|
#telegram section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -969,6 +971,12 @@ DisableWebPagePreview=false
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
UseFirstName=false
|
UseFirstName=false
|
||||||
|
|
||||||
|
#If enabled use the "Full Name" as username. If this is empty use the Username
|
||||||
|
#If disabled use the "Username" as username. If this is empty use the First Name and Last Name as Full Name
|
||||||
|
#If all names are empty, username will be "unknown"
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
UseFullName=false
|
||||||
|
|
||||||
#WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs
|
#WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs
|
||||||
#Those URLs will contain your bot-token. This may not be what you want.
|
#Those URLs will contain your bot-token. This may not be what you want.
|
||||||
#For now there is no secure way to relay GIF/stickers/documents without seeing your token.
|
#For now there is no secure way to relay GIF/stickers/documents without seeing your token.
|
||||||
@ -992,6 +1000,13 @@ QuoteFormat="{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
MediaConvertWebPToPNG=false
|
MediaConvertWebPToPNG=false
|
||||||
|
|
||||||
|
#Convert Tgs (Telegram animated sticker) images to PNG before upload.
|
||||||
|
#This is useful when your bridge also contains platforms that do not support animated WebP files, like Discord.
|
||||||
|
#This requires the external dependency `lottie`, which can be installed like this:
|
||||||
|
#`pip install lottie cairosvg`
|
||||||
|
#https://github.com/42wim/matterbridge/issues/874
|
||||||
|
#MediaConvertTgs="png"
|
||||||
|
|
||||||
#Disable sending of edits to other bridges
|
#Disable sending of edits to other bridges
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
EditDisable=false
|
EditDisable=false
|
||||||
@ -1025,7 +1040,7 @@ IgnoreMessages="^~~ badword"
|
|||||||
ReplaceMessages=[ ["cat","dog"] ]
|
ReplaceMessages=[ ["cat","dog"] ]
|
||||||
|
|
||||||
#nicks you want to replace.
|
#nicks you want to replace.
|
||||||
#see replacemessages for syntaxa
|
#see replacemessages for syntax
|
||||||
#optional (default empty)
|
#optional (default empty)
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
ReplaceNicks=[ ["user--","user"] ]
|
||||||
|
|
||||||
@ -1053,7 +1068,7 @@ Label=""
|
|||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
#Enable to show users joins/parts from other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
@ -1067,6 +1082,12 @@ StripNick=false
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowTopicChange=false
|
ShowTopicChange=false
|
||||||
|
|
||||||
|
#Opportunistically preserve threaded replies between Telegram groups.
|
||||||
|
#This only works if the parent message is still in the cache.
|
||||||
|
#Cache is flushed between restarts.
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
PreserveThreading=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#rocketchat section
|
#rocketchat section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -1160,7 +1181,7 @@ IgnoreMessages="^~~ badword"
|
|||||||
ReplaceMessages=[ ["cat","dog"] ]
|
ReplaceMessages=[ ["cat","dog"] ]
|
||||||
|
|
||||||
#nicks you want to replace.
|
#nicks you want to replace.
|
||||||
#see replacemessages for syntaxa
|
#see replacemessages for syntax
|
||||||
#optional (default empty)
|
#optional (default empty)
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
ReplaceNicks=[ ["user--","user"] ]
|
||||||
|
|
||||||
@ -1184,7 +1205,7 @@ Label=""
|
|||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
#Enable to show users joins/parts from other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
@ -1211,12 +1232,16 @@ ShowTopicChange=false
|
|||||||
#REQUIRED
|
#REQUIRED
|
||||||
Server="https://matrix.org"
|
Server="https://matrix.org"
|
||||||
|
|
||||||
#login/pass of your bot.
|
#Authentication for your bot.
|
||||||
|
#You can use either login/password OR mxid/token. The latter will be preferred if found.
|
||||||
#Use a dedicated user for this and not your own!
|
#Use a dedicated user for this and not your own!
|
||||||
#Messages sent from this user will not be relayed to avoid loops.
|
#Messages sent from this user will not be relayed to avoid loops.
|
||||||
#REQUIRED
|
#REQUIRED
|
||||||
Login="yourlogin"
|
Login="yourlogin"
|
||||||
Password="yourpass"
|
Password="yourpass"
|
||||||
|
#OR
|
||||||
|
MxID="@yourlogin:domain.tld"
|
||||||
|
Token="tokenforthebotuser"
|
||||||
|
|
||||||
#Whether to send the homeserver suffix. eg ":matrix.org" in @username:matrix.org
|
#Whether to send the homeserver suffix. eg ":matrix.org" in @username:matrix.org
|
||||||
#to other bridges, or only send "username".(true only sends username)
|
#to other bridges, or only send "username".(true only sends username)
|
||||||
@ -1234,12 +1259,14 @@ HTMLDisable=false
|
|||||||
# UseUserName shows the username instead of the server nickname
|
# UseUserName shows the username instead of the server nickname
|
||||||
UseUserName=false
|
UseUserName=false
|
||||||
|
|
||||||
#Whether to prefix messages from other bridges to matrix with the sender's nick.
|
# Matrix quotes replies and as of matterbridge 1.24.0 we strip those as this causes
|
||||||
#Useful if username overrides for incoming webhooks isn't enabled on the
|
# issues with bridges support threading and have PreserveThreading enabled.
|
||||||
#matrix server. If you set PrefixMessagesWithNick to true, each message
|
# But if you for example use mattermost or discord with webhooks you'll need to enable
|
||||||
#from bridge to matrix will by default be prefixed by the RemoteNickFormat setting. i
|
# this (and keep PreserveThreading disabled) if you want something that looks like a reply from matrix.
|
||||||
#OPTIONAL (default false)
|
# See issues:
|
||||||
PrefixMessagesWithNick=false
|
# - https://github.com/42wim/matterbridge/issues/1819
|
||||||
|
# - https://github.com/42wim/matterbridge/issues/1780
|
||||||
|
KeepQuotedReply=false
|
||||||
|
|
||||||
#Nicks you want to ignore.
|
#Nicks you want to ignore.
|
||||||
#Regular expressions supported
|
#Regular expressions supported
|
||||||
@ -1266,7 +1293,7 @@ IgnoreMessages="^~~ badword"
|
|||||||
ReplaceMessages=[ ["cat","dog"] ]
|
ReplaceMessages=[ ["cat","dog"] ]
|
||||||
|
|
||||||
#nicks you want to replace.
|
#nicks you want to replace.
|
||||||
#see replacemessages for syntaxa
|
#see replacemessages for syntax
|
||||||
#optional (default empty)
|
#optional (default empty)
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
ReplaceNicks=[ ["user--","user"] ]
|
||||||
|
|
||||||
@ -1290,10 +1317,15 @@ Label=""
|
|||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
#Enable to show users joins/parts from other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#Rename the bot in the current room to the username of the message
|
||||||
|
#This will make an additional API request per message and will probably count towards rate limits
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
SpoofUsername=false
|
||||||
|
|
||||||
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
#It will strip other characters from the nick
|
#It will strip other characters from the nick
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
@ -1358,7 +1390,7 @@ IgnoreMessages="^~~ badword"
|
|||||||
ReplaceMessages=[ ["cat","dog"] ]
|
ReplaceMessages=[ ["cat","dog"] ]
|
||||||
|
|
||||||
#nicks you want to replace.
|
#nicks you want to replace.
|
||||||
#see replacemessages for syntaxa
|
#see replacemessages for syntax
|
||||||
#optional (default empty)
|
#optional (default empty)
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
ReplaceNicks=[ ["user--","user"] ]
|
||||||
|
|
||||||
@ -1382,7 +1414,7 @@ Label=""
|
|||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
#Enable to show users joins/parts from other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
@ -1397,9 +1429,7 @@ StripNick=false
|
|||||||
ShowTopicChange=false
|
ShowTopicChange=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#
|
|
||||||
# NCTalk (Nextcloud Talk)
|
# NCTalk (Nextcloud Talk)
|
||||||
#
|
|
||||||
###################################################################
|
###################################################################
|
||||||
|
|
||||||
[nctalk.bridge]
|
[nctalk.bridge]
|
||||||
@ -1421,10 +1451,11 @@ Password = "talkuserpass"
|
|||||||
# Suffix for Guest Users
|
# Suffix for Guest Users
|
||||||
GuestSuffix = " (Guest)"
|
GuestSuffix = " (Guest)"
|
||||||
|
|
||||||
|
# Separate display name (Note: needs to be configured from Nextcloud Talk to work)
|
||||||
|
SeparateDisplayName=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#
|
|
||||||
# Mumble
|
# Mumble
|
||||||
#
|
|
||||||
###################################################################
|
###################################################################
|
||||||
|
|
||||||
[mumble.bridge]
|
[mumble.bridge]
|
||||||
@ -1435,7 +1466,7 @@ Server = "mumble.yourdomain.me:64738"
|
|||||||
# Nickname to log in as
|
# Nickname to log in as
|
||||||
Nick = "matterbridge"
|
Nick = "matterbridge"
|
||||||
|
|
||||||
# Some servers require a password
|
# Some servers require a password
|
||||||
# OPTIONAL (default empty)
|
# OPTIONAL (default empty)
|
||||||
Password = "serverpasswordhere"
|
Password = "serverpasswordhere"
|
||||||
|
|
||||||
@ -1467,10 +1498,30 @@ TLSCACertificate=mumble-ca.crt
|
|||||||
# OPTIONAL (default false)
|
# OPTIONAL (default false)
|
||||||
SkipTLSVerify=false
|
SkipTLSVerify=false
|
||||||
|
|
||||||
|
#Message to show when a message is too big
|
||||||
|
#Default "<clipped message>"
|
||||||
|
MessageClipped="<clipped message>"
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#Do not send joins/parts to other bridges
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
NoSendJoinPart=false
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#VK
|
||||||
###################################################################
|
###################################################################
|
||||||
#
|
#
|
||||||
|
[vk.myvk]
|
||||||
|
#Group access token
|
||||||
|
#See https://vk.com/dev/bots_docs
|
||||||
|
Token="Yourtokenhere"
|
||||||
|
|
||||||
|
###################################################################
|
||||||
# WhatsApp
|
# WhatsApp
|
||||||
#
|
|
||||||
###################################################################
|
###################################################################
|
||||||
|
|
||||||
[whatsapp.bridge]
|
[whatsapp.bridge]
|
||||||
@ -1497,9 +1548,7 @@ Label="Organization"
|
|||||||
|
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#
|
|
||||||
# zulip
|
# zulip
|
||||||
#
|
|
||||||
###################################################################
|
###################################################################
|
||||||
|
|
||||||
[zulip]
|
[zulip]
|
||||||
@ -1550,7 +1599,7 @@ IgnoreMessages="^~~ badword"
|
|||||||
ReplaceMessages=[ ["cat","dog"] ]
|
ReplaceMessages=[ ["cat","dog"] ]
|
||||||
|
|
||||||
#nicks you want to replace.
|
#nicks you want to replace.
|
||||||
#see replacemessages for syntaxa
|
#see replacemessages for syntax
|
||||||
#optional (default empty)
|
#optional (default empty)
|
||||||
ReplaceNicks=[ ["user--","user"] ]
|
ReplaceNicks=[ ["user--","user"] ]
|
||||||
|
|
||||||
@ -1574,7 +1623,7 @@ Label=""
|
|||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
#Enable to show users joins/parts from other bridges
|
#Enable to show users joins/parts from other bridges
|
||||||
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
|
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
@ -1588,6 +1637,18 @@ StripNick=false
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowTopicChange=false
|
ShowTopicChange=false
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
# Harmony
|
||||||
|
###################################################################
|
||||||
|
|
||||||
|
[harmony.chat_harmonyapp_io]
|
||||||
|
Homeserver = "https://chat.harmonyapp.io:2289"
|
||||||
|
Token = "your token goes here"
|
||||||
|
UserID = "user id of the bot account"
|
||||||
|
Community = "community id that channels will be located in"
|
||||||
|
UseUserName = true
|
||||||
|
RemoteNickFormat = "{NICK}"
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#API
|
#API
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -1632,7 +1693,9 @@ RemoteNickFormat="{NICK}"
|
|||||||
## Settings below can be reloaded by editing the file
|
## Settings below can be reloaded by editing the file
|
||||||
|
|
||||||
#RemoteNickFormat defines how remote users appear on this bridge
|
#RemoteNickFormat defines how remote users appear on this bridge
|
||||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
#The string "{NICK}" (case sensitive) will be replaced by the actual nick.
|
||||||
|
#The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged.
|
||||||
|
#The string "{USERID}" (case sensitive) will be replaced by the user ID.
|
||||||
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||||
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
|
||||||
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
@ -1658,7 +1721,7 @@ StripNick=false
|
|||||||
#The MediaServerDownload will be used so that bridges without native uploading support:
|
#The MediaServerDownload will be used so that bridges without native uploading support:
|
||||||
#gitter, irc and xmpp will be shown links to the files on MediaServerDownload
|
#gitter, irc and xmpp will be shown links to the files on MediaServerDownload
|
||||||
#
|
#
|
||||||
#More information https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D
|
#More information https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%28advanced%29
|
||||||
#OPTIONAL (default empty)
|
#OPTIONAL (default empty)
|
||||||
MediaServerUpload="https://user:pass@yourserver.com/upload"
|
MediaServerUpload="https://user:pass@yourserver.com/upload"
|
||||||
#OPTIONAL (default empty)
|
#OPTIONAL (default empty)
|
||||||
@ -1707,7 +1770,7 @@ LogFile="/var/log/matterbridge.log"
|
|||||||
#This script will receive every incoming message and can be used to modify the Username and the Text of that message.
|
#This script will receive every incoming message and can be used to modify the Username and the Text of that message.
|
||||||
#The script will have the following global variables:
|
#The script will have the following global variables:
|
||||||
#to modify: msgUsername and msgText
|
#to modify: msgUsername and msgText
|
||||||
#to read: msgChannel and msgAccount
|
#to read: msgUserID, msgChannel, msgAccount
|
||||||
#
|
#
|
||||||
#The script is reloaded on every message, so you can modify the script on the fly.
|
#The script is reloaded on every message, so you can modify the script on the fly.
|
||||||
#
|
#
|
||||||
@ -1731,6 +1794,7 @@ InMessage="example.tengo"
|
|||||||
#read-only:
|
#read-only:
|
||||||
#inAccount, inProtocol, inChannel, inGateway, inEvent
|
#inAccount, inProtocol, inChannel, inGateway, inEvent
|
||||||
#outAccount, outProtocol, outChannel, outGateway, outEvent
|
#outAccount, outProtocol, outChannel, outGateway, outEvent
|
||||||
|
#msgUserID
|
||||||
#
|
#
|
||||||
#read-write:
|
#read-write:
|
||||||
#msgText, msgUsername, msgDrop
|
#msgText, msgUsername, msgDrop
|
||||||
@ -1748,7 +1812,7 @@ OutMessage="example.tengo"
|
|||||||
#RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script.
|
#RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script.
|
||||||
#The script will have the following global variables:
|
#The script will have the following global variables:
|
||||||
#to modify: result
|
#to modify: result
|
||||||
#to read: channel, bridge, gateway, protocol, nick
|
#to read: channel, bridge, gateway, protocol, nick, msgUserID
|
||||||
#
|
#
|
||||||
#The result will be set in {TENGO} in the RemoteNickFormat key of every bridge where {TENGO} is specified
|
#The result will be set in {TENGO} in the RemoteNickFormat key of every bridge where {TENGO} is specified
|
||||||
#
|
#
|
||||||
@ -1786,7 +1850,7 @@ enable=true
|
|||||||
|
|
||||||
# account specified above
|
# account specified above
|
||||||
# REQUIRED
|
# REQUIRED
|
||||||
account="irc.freenode"
|
account="irc.libera"
|
||||||
|
|
||||||
# The channel key in each gateway is mapped to a similar group chat ID on the chat platform
|
# The channel key in each gateway is mapped to a similar group chat ID on the chat platform
|
||||||
# To find the group chat ID for different platforms, refer to the table below
|
# To find the group chat ID for different platforms, refer to the table below
|
||||||
@ -1803,7 +1867,8 @@ enable=true
|
|||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
# irc | channel | #general | The # symbol is required and should be lowercase!
|
# irc | channel | #general | The # symbol is required and should be lowercase!
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
# mattermost | channel | general | This is the channel name as seen in the URL, not the display name
|
# | channel | general | This is the channel name as seen in the URL, not the display name
|
||||||
|
# mattermost | channel id | ID:oc4wifyuojgw5f3nsuweesmz8w | This is the channel ID (only use if you know what you're doing)
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
# matrix | #channel:server | #yourchannel:matrix.org | Encrypted rooms are not supported in matrix
|
# matrix | #channel:server | #yourchannel:matrix.org | Encrypted rooms are not supported in matrix
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
@ -1814,20 +1879,23 @@ enable=true
|
|||||||
# rocketchat | channel | #channel | # is required for private channels too
|
# rocketchat | channel | #channel | # is required for private channels too
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
# slack | channel name | general | Do not include the # symbol
|
# slack | channel name | general | Do not include the # symbol
|
||||||
# | channel id | ID:C123456 | The underlying ID of a channel. This doesn't work with
|
# | channel id | ID:C123456 | The underlying ID of a channel. This doesn't work with webhooks.
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
# steam | chatid | example needed | The number in the URL when you click "enter chat room" in the browser
|
# steam | chatid | example needed | The number in the URL when you click "enter chat room" in the browser
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
# nctalk | token | xs25tz5y | The token in the URL when you are in a chat. It will be the last part of the URL.
|
# nctalk | token | xs25tz5y | The token in the URL when you are in a chat. It will be the last part of the URL.
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
# telegram | chatid | -123456789 | A large negative number. see https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau
|
# telegram | chatid | -123456789 | A large negative number. see https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau
|
||||||
|
# | chatid/topicid | -123456789/12 | A large negative number/number.
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
# vk | peerid | 2000000002 | A number that starts form 2000000000. Use --debug and send any message in chat to get PeerID in the logs
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
# whatsapp | group JID | 48111222333-123455678999@g.us | A unique group JID. If you specify an empty string, bridge will list all the possibilities
|
# whatsapp | group JID | 48111222333-123455678999@g.us | A unique group JID. If you specify an empty string, bridge will list all the possibilities
|
||||||
# | "Group Name" | "Family Chat" | if you specify a group name, the bridge will find hint the JID to specify. Names can change over time and are not stable.
|
# | "Group Name" | "Family Chat" | if you specify a group name, the bridge will find hint the JID to specify. Names can change over time and are not stable.
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
# xmpp | channel | general | The room name
|
# xmpp | channel | general | The room name
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
# zulip | stream/topic:topic | general/off-topic:food | Do not use the # when specifying a topic
|
# zulip | stream/topic:topic | general/topic:food | Do not use the # when specifying a topic
|
||||||
# -------------------------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1842,7 +1910,7 @@ enable=true
|
|||||||
|
|
||||||
#[[gateway.out]] specifies the account and channels we will sent messages to.
|
#[[gateway.out]] specifies the account and channels we will sent messages to.
|
||||||
[[gateway.out]]
|
[[gateway.out]]
|
||||||
account="irc.freenode"
|
account="irc.libera"
|
||||||
channel="#testing"
|
channel="#testing"
|
||||||
|
|
||||||
#OPTIONAL - only used for IRC and XMPP protocols at the moment
|
#OPTIONAL - only used for IRC and XMPP protocols at the moment
|
||||||
@ -1861,18 +1929,25 @@ enable=true
|
|||||||
#OPTIONAL - your irc / xmpp channel key
|
#OPTIONAL - your irc / xmpp channel key
|
||||||
key="yourkey"
|
key="yourkey"
|
||||||
|
|
||||||
|
# Discord specific gateway options
|
||||||
[[gateway.inout]]
|
[[gateway.inout]]
|
||||||
account="discord.game"
|
account="discord.game"
|
||||||
channel="mygreatgame"
|
channel="mygreatgame"
|
||||||
|
|
||||||
#OPTIONAL - webhookurl only works for discord (it needs a different URL for each cahnnel)
|
|
||||||
[gateway.inout.options]
|
[gateway.inout.options]
|
||||||
webhookurl="https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y"
|
# WebhookURL sends messages in the style of "puppets". You must configure a webhook URL for each channel you want to bridge.
|
||||||
|
# If you have more than one channel and don't wnat to configure each channel manually, see the "AutoWebhooks" option in the gateway config.
|
||||||
|
# Example: "https://discord.com/api/webhooks/1234/abcd_xyzw"
|
||||||
|
WebhookURL=""
|
||||||
|
|
||||||
[[gateway.inout]]
|
[[gateway.inout]]
|
||||||
account="zulip.streamchat"
|
account="zulip.streamchat"
|
||||||
channel="general/topic:mytopic"
|
channel="general/topic:mytopic"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="harmony.chat_harmonyapp_io"
|
||||||
|
channel="channel id goes here"
|
||||||
|
|
||||||
#API example
|
#API example
|
||||||
#[[gateway.inout]]
|
#[[gateway.inout]]
|
||||||
#account="api.local"
|
#account="api.local"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#WARNING: as this file contains credentials, be sure to set correct file permissions
|
#WARNING: as this file contains credentials, be sure to set correct file permissions
|
||||||
[irc]
|
[irc]
|
||||||
[irc.freenode]
|
[irc.libera]
|
||||||
Server="irc.freenode.net:6667"
|
Server="irc.libera.chat:6667"
|
||||||
Nick="matterbot"
|
Nick="matterbot"
|
||||||
|
|
||||||
[mattermost]
|
[mattermost]
|
||||||
@ -17,7 +17,7 @@
|
|||||||
name="gateway1"
|
name="gateway1"
|
||||||
enable=true
|
enable=true
|
||||||
[[gateway.inout]]
|
[[gateway.inout]]
|
||||||
account="irc.freenode"
|
account="irc.libera"
|
||||||
channel="#testing"
|
channel="#testing"
|
||||||
|
|
||||||
[[gateway.inout]]
|
[[gateway.inout]]
|
||||||
@ -29,6 +29,6 @@ enable=true
|
|||||||
#name="gateway2"
|
#name="gateway2"
|
||||||
#enable=true
|
#enable=true
|
||||||
#inout = [
|
#inout = [
|
||||||
# { account="irc.freenode", channel="#testing", options={key="channelkey"}},
|
# { account="irc.libera", channel="#testing", options={key="channelkey"}},
|
||||||
# { account="mattermost.work", channel="off-topic" },
|
# { account="mattermost.work", channel="off-topic" },
|
||||||
#]
|
#]
|
||||||
|
1
matterclient/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
Find matterclient on https://github.com/matterbridge/matterclient
|
@ -1,226 +0,0 @@
|
|||||||
package matterclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetChannels returns all channels we're members off
|
|
||||||
func (m *MMClient) GetChannels() []*model.Channel {
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
var channels []*model.Channel
|
|
||||||
// our primary team channels first
|
|
||||||
channels = append(channels, m.Team.Channels...)
|
|
||||||
for _, t := range m.OtherTeams {
|
|
||||||
if t.Id != m.Team.Id {
|
|
||||||
channels = append(channels, t.Channels...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return channels
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetChannelHeader(channelId string) string { //nolint:golint
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
for _, t := range m.OtherTeams {
|
|
||||||
for _, channel := range append(t.Channels, t.MoreChannels...) {
|
|
||||||
if channel.Id == channelId {
|
|
||||||
return channel.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNormalisedName(channel *model.Channel) string {
|
|
||||||
if channel.Type == model.CHANNEL_GROUP {
|
|
||||||
// (deprecated in favor of ReplaceAll in go 1.12)
|
|
||||||
res := strings.Replace(channel.DisplayName, ", ", "-", -1) //nolint: gocritic
|
|
||||||
res = strings.Replace(res, " ", "_", -1) //nolint: gocritic
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
return channel.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
if teamId != "" {
|
|
||||||
return m.getChannelIdTeam(name, teamId)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, t := range m.OtherTeams {
|
|
||||||
for _, channel := range append(t.Channels, t.MoreChannels...) {
|
|
||||||
if getNormalisedName(channel) == name {
|
|
||||||
return channel.Id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) getChannelIdTeam(name string, teamId string) string { //nolint:golint
|
|
||||||
for _, t := range m.OtherTeams {
|
|
||||||
if t.Id == teamId {
|
|
||||||
for _, channel := range append(t.Channels, t.MoreChannels...) {
|
|
||||||
if getNormalisedName(channel) == name {
|
|
||||||
return channel.Id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetChannelName(channelId string) string { //nolint:golint
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
for _, t := range m.OtherTeams {
|
|
||||||
if t == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, channel := range append(t.Channels, t.MoreChannels...) {
|
|
||||||
if channel.Id == channelId {
|
|
||||||
return getNormalisedName(channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetChannelTeamId(id string) string { //nolint:golint
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
for _, t := range append(m.OtherTeams, m.Team) {
|
|
||||||
for _, channel := range append(t.Channels, t.MoreChannels...) {
|
|
||||||
if channel.Id == id {
|
|
||||||
return channel.TeamId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetLastViewedAt(channelId string) int64 { //nolint:golint
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return model.GetMillis()
|
|
||||||
}
|
|
||||||
return res.LastViewedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMoreChannels returns existing channels where we're not a member off.
|
|
||||||
func (m *MMClient) GetMoreChannels() []*model.Channel {
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
var channels []*model.Channel
|
|
||||||
for _, t := range m.OtherTeams {
|
|
||||||
channels = append(channels, t.MoreChannels...)
|
|
||||||
}
|
|
||||||
return channels
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId).
|
|
||||||
func (m *MMClient) GetTeamFromChannel(channelId string) string { //nolint:golint
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
var channels []*model.Channel
|
|
||||||
for _, t := range m.OtherTeams {
|
|
||||||
channels = append(channels, t.Channels...)
|
|
||||||
if t.MoreChannels != nil {
|
|
||||||
channels = append(channels, t.MoreChannels...)
|
|
||||||
}
|
|
||||||
for _, c := range channels {
|
|
||||||
if c.Id == channelId {
|
|
||||||
if c.Type == model.CHANNEL_GROUP {
|
|
||||||
return "G"
|
|
||||||
}
|
|
||||||
return t.Id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
channels = nil
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
for _, c := range m.Team.Channels {
|
|
||||||
if c.Id == channelId {
|
|
||||||
m.logger.Debug("Not joining ", channelId, " already joined.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.logger.Debug("Joining ", channelId)
|
|
||||||
_, resp := m.Client.AddChannelMember(channelId, m.User.Id)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) UpdateChannelsTeam(teamID string) error {
|
|
||||||
mmchannels, resp := m.Client.GetChannelsForTeamForUser(teamID, m.User.Id, false, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return errors.New(resp.Error.DetailedError)
|
|
||||||
}
|
|
||||||
for idx, t := range m.OtherTeams {
|
|
||||||
if t.Id == teamID {
|
|
||||||
m.Lock()
|
|
||||||
m.OtherTeams[idx].Channels = mmchannels
|
|
||||||
m.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mmchannels, resp = m.Client.GetPublicChannelsForTeam(teamID, 0, 5000, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return errors.New(resp.Error.DetailedError)
|
|
||||||
}
|
|
||||||
for idx, t := range m.OtherTeams {
|
|
||||||
if t.Id == teamID {
|
|
||||||
m.Lock()
|
|
||||||
m.OtherTeams[idx].MoreChannels = mmchannels
|
|
||||||
m.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) UpdateChannels() error {
|
|
||||||
if err := m.UpdateChannelsTeam(m.Team.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, t := range m.OtherTeams {
|
|
||||||
if err := m.UpdateChannelsTeam(t.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) UpdateChannelHeader(channelId string, header string) { //nolint:golint
|
|
||||||
channel := &model.Channel{Id: channelId, Header: header}
|
|
||||||
m.logger.Debugf("updating channelheader %#v, %#v", channelId, header)
|
|
||||||
_, resp := m.Client.UpdateChannel(channel)
|
|
||||||
if resp.Error != nil {
|
|
||||||
m.logger.Error(resp.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) UpdateLastViewed(channelId string) error { //nolint:golint
|
|
||||||
m.logger.Debugf("posting lastview %#v", channelId)
|
|
||||||
view := &model.ChannelView{ChannelId: channelId}
|
|
||||||
_, resp := m.Client.ViewChannel(m.User.Id, view)
|
|
||||||
if resp.Error != nil {
|
|
||||||
m.logger.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error)
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,297 +0,0 @@
|
|||||||
package matterclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/md5" //nolint:gosec
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/cookiejar"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/jpillora/backoff"
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error {
|
|
||||||
var resp *model.Response
|
|
||||||
var appErr *model.AppError
|
|
||||||
var logmsg = "trying login"
|
|
||||||
var err error
|
|
||||||
for {
|
|
||||||
m.logger.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
|
|
||||||
if m.Credentials.Token != "" {
|
|
||||||
resp, err = m.doLoginToken()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
|
|
||||||
}
|
|
||||||
appErr = resp.Error
|
|
||||||
if appErr != nil {
|
|
||||||
d := b.Duration()
|
|
||||||
m.logger.Debug(appErr.DetailedError)
|
|
||||||
if firstConnection {
|
|
||||||
if appErr.Message == "" {
|
|
||||||
return errors.New(appErr.DetailedError)
|
|
||||||
}
|
|
||||||
return errors.New(appErr.Message)
|
|
||||||
}
|
|
||||||
m.logger.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
|
|
||||||
time.Sleep(d)
|
|
||||||
logmsg = "retrying login"
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// reset timer
|
|
||||||
b.Reset()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) doLoginToken() (*model.Response, error) {
|
|
||||||
var resp *model.Response
|
|
||||||
var logmsg = "trying login"
|
|
||||||
m.Client.AuthType = model.HEADER_BEARER
|
|
||||||
m.Client.AuthToken = m.Credentials.Token
|
|
||||||
if m.Credentials.CookieToken {
|
|
||||||
m.logger.Debugf(logmsg + " with cookie (MMAUTH) token")
|
|
||||||
m.Client.HttpClient.Jar = m.createCookieJar(m.Credentials.Token)
|
|
||||||
} else {
|
|
||||||
m.logger.Debugf(logmsg + " with personal token")
|
|
||||||
}
|
|
||||||
m.User, resp = m.Client.GetMe("")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp, resp.Error
|
|
||||||
}
|
|
||||||
if m.User == nil {
|
|
||||||
m.logger.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
|
|
||||||
return resp, errors.New("invalid token")
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) handleLoginToken() error {
|
|
||||||
switch {
|
|
||||||
case strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN):
|
|
||||||
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
|
|
||||||
if len(token) != 2 {
|
|
||||||
return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
|
|
||||||
}
|
|
||||||
m.Credentials.Token = token[1]
|
|
||||||
m.Credentials.CookieToken = true
|
|
||||||
case strings.Contains(m.Credentials.Pass, "token="):
|
|
||||||
token := strings.Split(m.Credentials.Pass, "token=")
|
|
||||||
if len(token) != 2 {
|
|
||||||
return errors.New("incorrect personal token. valid input is token=yourtoken")
|
|
||||||
}
|
|
||||||
m.Credentials.Token = token[1]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) initClient(firstConnection bool, b *backoff.Backoff) error {
|
|
||||||
uriScheme := "https://"
|
|
||||||
if m.NoTLS {
|
|
||||||
uriScheme = "http://"
|
|
||||||
}
|
|
||||||
// login to mattermost
|
|
||||||
m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server)
|
|
||||||
m.Client.HttpClient.Transport = &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
}
|
|
||||||
m.Client.HttpClient.Timeout = time.Second * 10
|
|
||||||
|
|
||||||
// handle MMAUTHTOKEN and personal token
|
|
||||||
if err := m.handleLoginToken(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if server alive, retry until
|
|
||||||
if err := m.serverAlive(firstConnection, b); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize user and teams
|
|
||||||
func (m *MMClient) initUser() error {
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
// we only load all team data on initial login.
|
|
||||||
// all other updates are for channels from our (primary) team only.
|
|
||||||
//m.logger.Debug("initUser(): loading all team data")
|
|
||||||
teams, resp := m.Client.GetTeamsForUser(m.User.Id, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
for _, team := range teams {
|
|
||||||
idx := 0
|
|
||||||
max := 200
|
|
||||||
usermap := make(map[string]*model.User)
|
|
||||||
mmusers, resp := m.Client.GetUsersInTeam(team.Id, idx, max, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return errors.New(resp.Error.DetailedError)
|
|
||||||
}
|
|
||||||
for len(mmusers) > 0 {
|
|
||||||
for _, user := range mmusers {
|
|
||||||
usermap[user.Id] = user
|
|
||||||
}
|
|
||||||
mmusers, resp = m.Client.GetUsersInTeam(team.Id, idx, max, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return errors.New(resp.Error.DetailedError)
|
|
||||||
}
|
|
||||||
idx++
|
|
||||||
time.Sleep(time.Millisecond * 200)
|
|
||||||
}
|
|
||||||
m.logger.Infof("found %d users in team %s", len(usermap), team.Name)
|
|
||||||
|
|
||||||
t := &Team{Team: team, Users: usermap, Id: team.Id}
|
|
||||||
|
|
||||||
mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, false, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
t.Channels = mmchannels
|
|
||||||
mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
t.MoreChannels = mmchannels
|
|
||||||
m.OtherTeams = append(m.OtherTeams, t)
|
|
||||||
if team.Name == m.Credentials.Team {
|
|
||||||
m.Team = t
|
|
||||||
m.logger.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id)
|
|
||||||
}
|
|
||||||
// add all users
|
|
||||||
for k, v := range t.Users {
|
|
||||||
m.Users[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error {
|
|
||||||
defer b.Reset()
|
|
||||||
for {
|
|
||||||
d := b.Duration()
|
|
||||||
// bogus call to get the serverversion
|
|
||||||
_, resp := m.Client.Logout()
|
|
||||||
if resp.Error != nil {
|
|
||||||
return fmt.Errorf("%#v", resp.Error.Error())
|
|
||||||
}
|
|
||||||
if firstConnection && !m.SkipVersionCheck && !supportedVersion(resp.ServerVersion) {
|
|
||||||
return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion)
|
|
||||||
}
|
|
||||||
if !m.SkipVersionCheck {
|
|
||||||
m.ServerVersion = resp.ServerVersion
|
|
||||||
if m.ServerVersion == "" {
|
|
||||||
m.logger.Debugf("Server not up yet, reconnecting in %s", d)
|
|
||||||
time.Sleep(d)
|
|
||||||
} else {
|
|
||||||
m.logger.Infof("Found version %s", m.ServerVersion)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) wsConnect() {
|
|
||||||
b := &backoff.Backoff{
|
|
||||||
Min: time.Second,
|
|
||||||
Max: 5 * time.Minute,
|
|
||||||
Jitter: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
m.WsConnected = false
|
|
||||||
wsScheme := "wss://"
|
|
||||||
if m.NoTLS {
|
|
||||||
wsScheme = "ws://"
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup websocket connection
|
|
||||||
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket"
|
|
||||||
header := http.Header{}
|
|
||||||
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
|
|
||||||
|
|
||||||
m.logger.Debugf("WsClient: making connection: %s", wsurl)
|
|
||||||
for {
|
|
||||||
wsDialer := &websocket.Dialer{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
|
|
||||||
if err != nil {
|
|
||||||
d := b.Duration()
|
|
||||||
m.logger.Debugf("WSS: %s, reconnecting in %s", err, d)
|
|
||||||
time.Sleep(d)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
m.logger.Debug("WsClient: connected")
|
|
||||||
m.WsSequence = 1
|
|
||||||
m.WsPingChan = make(chan *model.WebSocketResponse)
|
|
||||||
// only start to parse WS messages when login is completely done
|
|
||||||
m.WsConnected = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
|
|
||||||
var cookies []*http.Cookie
|
|
||||||
jar, _ := cookiejar.New(nil)
|
|
||||||
firstCookie := &http.Cookie{
|
|
||||||
Name: "MMAUTHTOKEN",
|
|
||||||
Value: token,
|
|
||||||
Path: "/",
|
|
||||||
Domain: m.Credentials.Server,
|
|
||||||
}
|
|
||||||
cookies = append(cookies, firstCookie)
|
|
||||||
cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
|
|
||||||
jar.SetCookies(cookieURL, cookies)
|
|
||||||
return jar
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) checkAlive() error {
|
|
||||||
// check if session still is valid
|
|
||||||
_, resp := m.Client.GetMe("")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
m.logger.Debug("WS PING")
|
|
||||||
return m.sendWSRequest("ping", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error {
|
|
||||||
req := &model.WebSocketRequest{}
|
|
||||||
req.Seq = m.WsSequence
|
|
||||||
req.Action = action
|
|
||||||
req.Data = data
|
|
||||||
m.WsSequence++
|
|
||||||
m.logger.Debugf("sendWsRequest %#v", req)
|
|
||||||
return m.WsClient.WriteJSON(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func supportedVersion(version string) bool {
|
|
||||||
if strings.HasPrefix(version, "3.8.0") ||
|
|
||||||
strings.HasPrefix(version, "3.9.0") ||
|
|
||||||
strings.HasPrefix(version, "3.10.0") ||
|
|
||||||
strings.HasPrefix(version, "4.") ||
|
|
||||||
strings.HasPrefix(version, "5.") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func digestString(s string) string {
|
|
||||||
return fmt.Sprintf("%x", md5.Sum([]byte(s))) //nolint:gosec
|
|
||||||
}
|
|
@ -1,294 +0,0 @@
|
|||||||
package matterclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
lru "github.com/hashicorp/golang-lru"
|
|
||||||
"github.com/jpillora/backoff"
|
|
||||||
prefixed "github.com/matterbridge/logrus-prefixed-formatter"
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Credentials struct {
|
|
||||||
Login string
|
|
||||||
Team string
|
|
||||||
Pass string
|
|
||||||
Token string
|
|
||||||
CookieToken bool
|
|
||||||
Server string
|
|
||||||
NoTLS bool
|
|
||||||
SkipTLSVerify bool
|
|
||||||
SkipVersionCheck bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
Raw *model.WebSocketEvent
|
|
||||||
Post *model.Post
|
|
||||||
Team string
|
|
||||||
Channel string
|
|
||||||
Username string
|
|
||||||
Text string
|
|
||||||
Type string
|
|
||||||
UserID string
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:golint
|
|
||||||
type Team struct {
|
|
||||||
Team *model.Team
|
|
||||||
Id string
|
|
||||||
Channels []*model.Channel
|
|
||||||
MoreChannels []*model.Channel
|
|
||||||
Users map[string]*model.User
|
|
||||||
}
|
|
||||||
|
|
||||||
type MMClient struct {
|
|
||||||
sync.RWMutex
|
|
||||||
*Credentials
|
|
||||||
|
|
||||||
Team *Team
|
|
||||||
OtherTeams []*Team
|
|
||||||
Client *model.Client4
|
|
||||||
User *model.User
|
|
||||||
Users map[string]*model.User
|
|
||||||
MessageChan chan *Message
|
|
||||||
WsClient *websocket.Conn
|
|
||||||
WsQuit bool
|
|
||||||
WsAway bool
|
|
||||||
WsConnected bool
|
|
||||||
WsSequence int64
|
|
||||||
WsPingChan chan *model.WebSocketResponse
|
|
||||||
ServerVersion string
|
|
||||||
OnWsConnect func()
|
|
||||||
|
|
||||||
logger *logrus.Entry
|
|
||||||
rootLogger *logrus.Logger
|
|
||||||
lruCache *lru.Cache
|
|
||||||
allevents bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// New will instantiate a new Matterclient with the specified login details without connecting.
|
|
||||||
func New(login string, pass string, team string, server string) *MMClient {
|
|
||||||
rootLogger := logrus.New()
|
|
||||||
rootLogger.SetFormatter(&prefixed.TextFormatter{
|
|
||||||
PrefixPadding: 13,
|
|
||||||
DisableColors: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
cred := &Credentials{
|
|
||||||
Login: login,
|
|
||||||
Pass: pass,
|
|
||||||
Team: team,
|
|
||||||
Server: server,
|
|
||||||
}
|
|
||||||
|
|
||||||
cache, _ := lru.New(500)
|
|
||||||
return &MMClient{
|
|
||||||
Credentials: cred,
|
|
||||||
MessageChan: make(chan *Message, 100),
|
|
||||||
Users: make(map[string]*model.User),
|
|
||||||
rootLogger: rootLogger,
|
|
||||||
lruCache: cache,
|
|
||||||
logger: rootLogger.WithFields(logrus.Fields{"prefix": "matterclient"}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDebugLog activates debugging logging on all Matterclient log output.
|
|
||||||
func (m *MMClient) SetDebugLog() {
|
|
||||||
m.rootLogger.SetFormatter(&prefixed.TextFormatter{
|
|
||||||
PrefixPadding: 13,
|
|
||||||
DisableColors: true,
|
|
||||||
FullTimestamp: false,
|
|
||||||
ForceFormatting: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLogLevel tries to parse the specified level and if successful sets
|
|
||||||
// the log level accordingly. Accepted levels are: 'debug', 'info', 'warn',
|
|
||||||
// 'error', 'fatal' and 'panic'.
|
|
||||||
func (m *MMClient) SetLogLevel(level string) {
|
|
||||||
l, err := logrus.ParseLevel(level)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Warnf("Failed to parse specified log-level '%s': %#v", level, err)
|
|
||||||
} else {
|
|
||||||
m.rootLogger.SetLevel(l)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) EnableAllEvents() {
|
|
||||||
m.allevents = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login tries to connect the client with the loging details with which it was initialized.
|
|
||||||
func (m *MMClient) Login() error {
|
|
||||||
// check if this is a first connect or a reconnection
|
|
||||||
firstConnection := true
|
|
||||||
if m.WsConnected {
|
|
||||||
firstConnection = false
|
|
||||||
}
|
|
||||||
m.WsConnected = false
|
|
||||||
if m.WsQuit {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b := &backoff.Backoff{
|
|
||||||
Min: time.Second,
|
|
||||||
Max: 5 * time.Minute,
|
|
||||||
Jitter: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// do initialization setup
|
|
||||||
if err := m.initClient(firstConnection, b); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.doLogin(firstConnection, b); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.initUser(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Team == nil {
|
|
||||||
validTeamNames := make([]string, len(m.OtherTeams))
|
|
||||||
for i, t := range m.OtherTeams {
|
|
||||||
validTeamNames[i] = t.Team.Name
|
|
||||||
}
|
|
||||||
return fmt.Errorf("Team '%s' not found in %v", m.Credentials.Team, validTeamNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.wsConnect()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout disconnects the client from the chat server.
|
|
||||||
func (m *MMClient) Logout() error {
|
|
||||||
m.logger.Debugf("logout as %s (team: %s) on %s", m.Credentials.Login, m.Credentials.Team, m.Credentials.Server)
|
|
||||||
m.WsQuit = true
|
|
||||||
m.WsClient.Close()
|
|
||||||
m.WsClient.UnderlyingConn().Close()
|
|
||||||
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
|
|
||||||
m.logger.Debug("Not invalidating session in logout, credential is a token")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
_, resp := m.Client.Logout()
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WsReceiver implements the core loop that manages the connection to the chat server. In
|
|
||||||
// case of a disconnect it will try to reconnect. A call to this method is blocking until
|
|
||||||
// the 'WsQuite' field of the MMClient object is set to 'true'.
|
|
||||||
func (m *MMClient) WsReceiver() {
|
|
||||||
for {
|
|
||||||
var rawMsg json.RawMessage
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if m.WsQuit {
|
|
||||||
m.logger.Debug("exiting WsReceiver")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !m.WsConnected {
|
|
||||||
time.Sleep(time.Millisecond * 100)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil {
|
|
||||||
m.logger.Error("error:", err)
|
|
||||||
// reconnect
|
|
||||||
m.wsConnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
var event model.WebSocketEvent
|
|
||||||
if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
|
|
||||||
m.logger.Debugf("WsReceiver event: %#v", event)
|
|
||||||
msg := &Message{Raw: &event, Team: m.Credentials.Team}
|
|
||||||
m.parseMessage(msg)
|
|
||||||
// check if we didn't empty the message
|
|
||||||
if msg.Text != "" {
|
|
||||||
m.MessageChan <- msg
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// if we have file attached but the message is empty, also send it
|
|
||||||
if msg.Post != nil {
|
|
||||||
if msg.Text != "" || len(msg.Post.FileIds) > 0 || msg.Post.Type == "slack_attachment" {
|
|
||||||
m.MessageChan <- msg
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if m.allevents {
|
|
||||||
m.MessageChan <- msg
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch msg.Raw.Event {
|
|
||||||
case model.WEBSOCKET_EVENT_USER_ADDED,
|
|
||||||
model.WEBSOCKET_EVENT_USER_REMOVED,
|
|
||||||
model.WEBSOCKET_EVENT_CHANNEL_CREATED,
|
|
||||||
model.WEBSOCKET_EVENT_CHANNEL_DELETED:
|
|
||||||
m.MessageChan <- msg
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var response model.WebSocketResponse
|
|
||||||
if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
|
|
||||||
m.logger.Debugf("WsReceiver response: %#v", response)
|
|
||||||
m.parseResponse(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatusLoop implements a ping-cycle that ensures that the connection to the chat servers
|
|
||||||
// remains alive. In case of a disconnect it will try to reconnect. A call to this method
|
|
||||||
// is blocking until the 'WsQuite' field of the MMClient object is set to 'true'.
|
|
||||||
func (m *MMClient) StatusLoop() {
|
|
||||||
retries := 0
|
|
||||||
backoff := time.Second * 60
|
|
||||||
if m.OnWsConnect != nil {
|
|
||||||
m.OnWsConnect()
|
|
||||||
}
|
|
||||||
m.logger.Debug("StatusLoop:", m.OnWsConnect != nil)
|
|
||||||
for {
|
|
||||||
if m.WsQuit {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if m.WsConnected {
|
|
||||||
if err := m.checkAlive(); err != nil {
|
|
||||||
m.logger.Errorf("Connection is not alive: %#v", err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-m.WsPingChan:
|
|
||||||
m.logger.Debug("WS PONG received")
|
|
||||||
backoff = time.Second * 60
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
if retries > 3 {
|
|
||||||
m.logger.Debug("StatusLoop() timeout")
|
|
||||||
m.Logout()
|
|
||||||
m.WsQuit = false
|
|
||||||
err := m.Login()
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Errorf("Login failed: %#v", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if m.OnWsConnect != nil {
|
|
||||||
m.OnWsConnect()
|
|
||||||
}
|
|
||||||
go m.WsReceiver()
|
|
||||||
} else {
|
|
||||||
retries++
|
|
||||||
backoff = time.Second * 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
time.Sleep(backoff)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,207 +0,0 @@
|
|||||||
package matterclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *MMClient) parseActionPost(rmsg *Message) {
|
|
||||||
// add post to cache, if it already exists don't relay this again.
|
|
||||||
// this should fix reposts
|
|
||||||
if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok {
|
|
||||||
m.logger.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string))
|
|
||||||
rmsg.Text = ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string)))
|
|
||||||
// we don't have the user, refresh the userlist
|
|
||||||
if m.GetUser(data.UserId) == nil {
|
|
||||||
m.logger.Infof("User '%v' is not known, ignoring message '%#v'",
|
|
||||||
data.UserId, data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rmsg.Username = m.GetUserName(data.UserId)
|
|
||||||
rmsg.Channel = m.GetChannelName(data.ChannelId)
|
|
||||||
rmsg.UserID = data.UserId
|
|
||||||
rmsg.Type = data.Type
|
|
||||||
teamid, _ := rmsg.Raw.Data["team_id"].(string)
|
|
||||||
// edit messsages have no team_id for some reason
|
|
||||||
if teamid == "" {
|
|
||||||
// we can find the team_id from the channelid
|
|
||||||
teamid = m.GetChannelTeamId(data.ChannelId)
|
|
||||||
rmsg.Raw.Data["team_id"] = teamid
|
|
||||||
}
|
|
||||||
if teamid != "" {
|
|
||||||
rmsg.Team = m.GetTeamName(teamid)
|
|
||||||
}
|
|
||||||
// direct message
|
|
||||||
if rmsg.Raw.Data["channel_type"] == "D" {
|
|
||||||
rmsg.Channel = m.GetUser(data.UserId).Username
|
|
||||||
}
|
|
||||||
rmsg.Text = data.Message
|
|
||||||
rmsg.Post = data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) parseMessage(rmsg *Message) {
|
|
||||||
switch rmsg.Raw.Event {
|
|
||||||
case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED:
|
|
||||||
m.parseActionPost(rmsg)
|
|
||||||
case "user_updated":
|
|
||||||
user := rmsg.Raw.Data["user"].(map[string]interface{})
|
|
||||||
if _, ok := user["id"].(string); ok {
|
|
||||||
m.UpdateUser(user["id"].(string))
|
|
||||||
}
|
|
||||||
case "group_added":
|
|
||||||
if err := m.UpdateChannels(); err != nil {
|
|
||||||
m.logger.Errorf("failed to update channels: %#v", err)
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
case model.ACTION_USER_REMOVED:
|
|
||||||
m.handleWsActionUserRemoved(&rmsg)
|
|
||||||
case model.ACTION_USER_ADDED:
|
|
||||||
m.handleWsActionUserAdded(&rmsg)
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) {
|
|
||||||
if rmsg.Data != nil {
|
|
||||||
// ping reply
|
|
||||||
if rmsg.Data["text"].(string) == "pong" {
|
|
||||||
m.WsPingChan <- &rmsg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) DeleteMessage(postId string) error { //nolint:golint
|
|
||||||
_, resp := m.Client.DeletePost(postId)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint
|
|
||||||
post := &model.Post{Message: text, Id: postId}
|
|
||||||
res, resp := m.Client.UpdatePost(postId, post)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return "", resp.Error
|
|
||||||
}
|
|
||||||
return res.Id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetFileLinks(filenames []string) []string {
|
|
||||||
uriScheme := "https://"
|
|
||||||
if m.NoTLS {
|
|
||||||
uriScheme = "http://"
|
|
||||||
}
|
|
||||||
|
|
||||||
var output []string
|
|
||||||
for _, f := range filenames {
|
|
||||||
res, resp := m.Client.GetFileLink(f)
|
|
||||||
if resp.Error != nil {
|
|
||||||
// public links is probably disabled, create the link ourselves
|
|
||||||
output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V4+"/files/"+f)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
output = append(output, res)
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList { //nolint:golint
|
|
||||||
res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList { //nolint:golint
|
|
||||||
res, resp := m.Client.GetPostsSince(channelId, time)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetPublicLink(filename string) string {
|
|
||||||
res, resp := m.Client.GetFileLink(filename)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetPublicLinks(filenames []string) []string {
|
|
||||||
var output []string
|
|
||||||
for _, f := range filenames {
|
|
||||||
res, resp := m.Client.GetFileLink(f)
|
|
||||||
if resp.Error != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
output = append(output, res)
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) PostMessage(channelId string, text string, rootId string) (string, error) { //nolint:golint
|
|
||||||
post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId}
|
|
||||||
res, resp := m.Client.CreatePost(post)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return "", resp.Error
|
|
||||||
}
|
|
||||||
return res.Id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) PostMessageWithFiles(channelId string, text string, rootId string, fileIds []string) (string, error) { //nolint:golint
|
|
||||||
post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId, FileIds: fileIds}
|
|
||||||
res, resp := m.Client.CreatePost(post)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return "", resp.Error
|
|
||||||
}
|
|
||||||
return res.Id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) SearchPosts(query string) *model.PostList {
|
|
||||||
res, resp := m.Client.SearchPosts(m.Team.Id, query, false)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendDirectMessage sends a direct message to specified user
|
|
||||||
func (m *MMClient) SendDirectMessage(toUserId string, msg string, rootId string) { //nolint:golint
|
|
||||||
m.SendDirectMessageProps(toUserId, msg, rootId, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) SendDirectMessageProps(toUserId string, msg string, rootId string, props map[string]interface{}) { //nolint:golint
|
|
||||||
m.logger.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
|
|
||||||
// create DM channel (only happens on first message)
|
|
||||||
_, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId)
|
|
||||||
if resp.Error != nil {
|
|
||||||
m.logger.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
|
|
||||||
|
|
||||||
// update our channels
|
|
||||||
if err := m.UpdateChannels(); err != nil {
|
|
||||||
m.logger.Errorf("failed to update channels: %#v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// build & send the message
|
|
||||||
msg = strings.Replace(msg, "\r", "", -1)
|
|
||||||
post := &model.Post{ChannelId: m.GetChannelId(channelName, m.Team.Id), Message: msg, RootId: rootId, Props: props}
|
|
||||||
m.Client.CreatePost(post)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) { //nolint:golint
|
|
||||||
f, resp := m.Client.UploadFile(data, channelId, filename)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return "", resp.Error
|
|
||||||
}
|
|
||||||
return f.FileInfos[0].Id, nil
|
|
||||||
}
|
|
@ -1,165 +0,0 @@
|
|||||||
package matterclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *MMClient) GetNickName(userId string) string { //nolint:golint
|
|
||||||
user := m.GetUser(userId)
|
|
||||||
if user != nil {
|
|
||||||
return user.Nickname
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetStatus(userId string) string { //nolint:golint
|
|
||||||
res, resp := m.Client.GetUserStatus(userId, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if res.Status == model.STATUS_AWAY {
|
|
||||||
return "away"
|
|
||||||
}
|
|
||||||
if res.Status == model.STATUS_ONLINE {
|
|
||||||
return "online"
|
|
||||||
}
|
|
||||||
return "offline"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetStatuses() map[string]string {
|
|
||||||
var ids []string
|
|
||||||
statuses := make(map[string]string)
|
|
||||||
for id := range m.Users {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
res, resp := m.Client.GetUsersStatusesByIds(ids)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return statuses
|
|
||||||
}
|
|
||||||
for _, status := range res {
|
|
||||||
statuses[status.UserId] = "offline"
|
|
||||||
if status.Status == model.STATUS_AWAY {
|
|
||||||
statuses[status.UserId] = "away"
|
|
||||||
}
|
|
||||||
if status.Status == model.STATUS_ONLINE {
|
|
||||||
statuses[status.UserId] = "online"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return statuses
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetTeamId() string { //nolint:golint
|
|
||||||
return m.Team.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTeamName returns the name of the specified teamId
|
|
||||||
func (m *MMClient) GetTeamName(teamId string) string { //nolint:golint
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
for _, t := range m.OtherTeams {
|
|
||||||
if t.Id == teamId {
|
|
||||||
return t.Team.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetUser(userId string) *model.User { //nolint:golint
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
_, ok := m.Users[userId]
|
|
||||||
if !ok {
|
|
||||||
res, resp := m.Client.GetUser(userId, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m.Users[userId] = res
|
|
||||||
}
|
|
||||||
return m.Users[userId]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetUserName(userId string) string { //nolint:golint
|
|
||||||
user := m.GetUser(userId)
|
|
||||||
if user != nil {
|
|
||||||
return user.Username
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) GetUsers() map[string]*model.User {
|
|
||||||
users := make(map[string]*model.User)
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
for k, v := range m.Users {
|
|
||||||
users[k] = v
|
|
||||||
}
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) UpdateUsers() error {
|
|
||||||
idx := 0
|
|
||||||
max := 200
|
|
||||||
mmusers, resp := m.Client.GetUsers(idx, max, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return errors.New(resp.Error.DetailedError)
|
|
||||||
}
|
|
||||||
for len(mmusers) > 0 {
|
|
||||||
m.Lock()
|
|
||||||
for _, user := range mmusers {
|
|
||||||
m.Users[user.Id] = user
|
|
||||||
}
|
|
||||||
m.Unlock()
|
|
||||||
mmusers, resp = m.Client.GetUsers(idx, max, "")
|
|
||||||
time.Sleep(time.Millisecond * 300)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return errors.New(resp.Error.DetailedError)
|
|
||||||
}
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) UpdateUserNick(nick string) error {
|
|
||||||
user := m.User
|
|
||||||
user.Nickname = nick
|
|
||||||
_, resp := m.Client.UpdateUser(user)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) UsernamesInChannel(channelId string) []string { //nolint:golint
|
|
||||||
res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
m.logger.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error)
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
allusers := m.GetUsers()
|
|
||||||
result := []string{}
|
|
||||||
for _, member := range *res {
|
|
||||||
result = append(result, allusers[member.UserId].Nickname)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) UpdateStatus(userId string, status string) error { //nolint:golint
|
|
||||||
_, resp := m.Client.UpdateUserStatus(userId, &model.Status{Status: status})
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MMClient) UpdateUser(userId string) { //nolint:golint
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
res, resp := m.Client.GetUser(userId, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.Users[userId] = res
|
|
||||||
}
|
|
@ -1,21 +1,18 @@
|
|||||||
FROM alpine AS builder
|
FROM alpine AS builder
|
||||||
|
|
||||||
COPY . /go/src/github.com/42wim/matterbridge
|
COPY . /go/src/matterbridge
|
||||||
RUN apk add \
|
RUN apk add \
|
||||||
go \
|
go \
|
||||||
git \
|
git \
|
||||||
gcc \
|
&& cd /go/src/matterbridge \
|
||||||
musl-dev \
|
&& 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
|
||||||
&& 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
|
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
cairo \
|
cairo \
|
||||||
libjpeg-turbo \
|
libjpeg-turbo \
|
||||||
|
libwebp-dev \
|
||||||
mailcap \
|
mailcap \
|
||||||
py3-webencodings \
|
py3-webencodings \
|
||||||
python3 \
|
python3 \
|
||||||
|
27
vendor/filippo.io/edwards25519/LICENSE
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
14
vendor/filippo.io/edwards25519/README.md
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# filippo.io/edwards25519
|
||||||
|
|
||||||
|
```
|
||||||
|
import "filippo.io/edwards25519"
|
||||||
|
```
|
||||||
|
|
||||||
|
This library implements the edwards25519 elliptic curve, exposing the necessary APIs to build a wide array of higher-level primitives.
|
||||||
|
Read the docs at [pkg.go.dev/filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519).
|
||||||
|
|
||||||
|
The code is originally derived from Adam Langley's internal implementation in the Go standard library, and includes George Tankersley's [performance improvements](https://golang.org/cl/71950). It was then further developed by Henry de Valence for use in ristretto255, and was finally [merged back into the Go standard library](https://golang.org/cl/276272) as of Go 1.17. It now tracks the upstream codebase and extends it with additional functionality.
|
||||||
|
|
||||||
|
Most users don't need this package, and should instead use `crypto/ed25519` for signatures, `golang.org/x/crypto/curve25519` for Diffie-Hellman, or `github.com/gtank/ristretto255` for prime order group logic. However, for anyone currently using a fork of `crypto/internal/edwards25519`/`crypto/ed25519/internal/edwards25519` or `github.com/agl/edwards25519`, this package should be a safer, faster, and more powerful alternative.
|
||||||
|
|
||||||
|
Since this package is meant to curb proliferation of edwards25519 implementations in the Go ecosystem, it welcomes requests for new APIs or reviewable performance improvements.
|
20
vendor/filippo.io/edwards25519/doc.go
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2021 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package edwards25519 implements group logic for the twisted Edwards curve
|
||||||
|
//
|
||||||
|
// -x^2 + y^2 = 1 + -(121665/121666)*x^2*y^2
|
||||||
|
//
|
||||||
|
// This is better known as the Edwards curve equivalent to Curve25519, and is
|
||||||
|
// the curve used by the Ed25519 signature scheme.
|
||||||
|
//
|
||||||
|
// Most users don't need this package, and should instead use crypto/ed25519 for
|
||||||
|
// signatures, golang.org/x/crypto/curve25519 for Diffie-Hellman, or
|
||||||
|
// github.com/gtank/ristretto255 for prime order group logic.
|
||||||
|
//
|
||||||
|
// However, developers who do need to interact with low-level edwards25519
|
||||||
|
// operations can use this package, which is an extended version of
|
||||||
|
// crypto/internal/edwards25519 from the standard library repackaged as
|
||||||
|
// an importable module.
|
||||||
|
package edwards25519
|
427
vendor/filippo.io/edwards25519/edwards25519.go
generated
vendored
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
// Copyright (c) 2017 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package edwards25519
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"filippo.io/edwards25519/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Point types.
|
||||||
|
|
||||||
|
type projP1xP1 struct {
|
||||||
|
X, Y, Z, T field.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
type projP2 struct {
|
||||||
|
X, Y, Z field.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point represents a point on the edwards25519 curve.
|
||||||
|
//
|
||||||
|
// This type works similarly to math/big.Int, and all arguments and receivers
|
||||||
|
// are allowed to alias.
|
||||||
|
//
|
||||||
|
// The zero value is NOT valid, and it may be used only as a receiver.
|
||||||
|
type Point struct {
|
||||||
|
// Make the type not comparable (i.e. used with == or as a map key), as
|
||||||
|
// equivalent points can be represented by different Go values.
|
||||||
|
_ incomparable
|
||||||
|
|
||||||
|
// The point is internally represented in extended coordinates (X, Y, Z, T)
|
||||||
|
// where x = X/Z, y = Y/Z, and xy = T/Z per https://eprint.iacr.org/2008/522.
|
||||||
|
x, y, z, t field.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
type incomparable [0]func()
|
||||||
|
|
||||||
|
func checkInitialized(points ...*Point) {
|
||||||
|
for _, p := range points {
|
||||||
|
if p.x == (field.Element{}) && p.y == (field.Element{}) {
|
||||||
|
panic("edwards25519: use of uninitialized Point")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type projCached struct {
|
||||||
|
YplusX, YminusX, Z, T2d field.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
type affineCached struct {
|
||||||
|
YplusX, YminusX, T2d field.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructors.
|
||||||
|
|
||||||
|
func (v *projP2) Zero() *projP2 {
|
||||||
|
v.X.Zero()
|
||||||
|
v.Y.One()
|
||||||
|
v.Z.One()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// identity is the point at infinity.
|
||||||
|
var identity, _ = new(Point).SetBytes([]byte{
|
||||||
|
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||||
|
|
||||||
|
// NewIdentityPoint returns a new Point set to the identity.
|
||||||
|
func NewIdentityPoint() *Point {
|
||||||
|
return new(Point).Set(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generator is the canonical curve basepoint. See TestGenerator for the
|
||||||
|
// correspondence of this encoding with the values in RFC 8032.
|
||||||
|
var generator, _ = new(Point).SetBytes([]byte{
|
||||||
|
0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||||
|
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||||
|
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||||
|
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66})
|
||||||
|
|
||||||
|
// NewGeneratorPoint returns a new Point set to the canonical generator.
|
||||||
|
func NewGeneratorPoint() *Point {
|
||||||
|
return new(Point).Set(generator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projCached) Zero() *projCached {
|
||||||
|
v.YplusX.One()
|
||||||
|
v.YminusX.One()
|
||||||
|
v.Z.One()
|
||||||
|
v.T2d.Zero()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *affineCached) Zero() *affineCached {
|
||||||
|
v.YplusX.One()
|
||||||
|
v.YminusX.One()
|
||||||
|
v.T2d.Zero()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignments.
|
||||||
|
|
||||||
|
// Set sets v = u, and returns v.
|
||||||
|
func (v *Point) Set(u *Point) *Point {
|
||||||
|
*v = *u
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoding.
|
||||||
|
|
||||||
|
// Bytes returns the canonical 32-byte encoding of v, according to RFC 8032,
|
||||||
|
// Section 5.1.2.
|
||||||
|
func (v *Point) Bytes() []byte {
|
||||||
|
// This function is outlined to make the allocations inline in the caller
|
||||||
|
// rather than happen on the heap.
|
||||||
|
var buf [32]byte
|
||||||
|
return v.bytes(&buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) bytes(buf *[32]byte) []byte {
|
||||||
|
checkInitialized(v)
|
||||||
|
|
||||||
|
var zInv, x, y field.Element
|
||||||
|
zInv.Invert(&v.z) // zInv = 1 / Z
|
||||||
|
x.Multiply(&v.x, &zInv) // x = X / Z
|
||||||
|
y.Multiply(&v.y, &zInv) // y = Y / Z
|
||||||
|
|
||||||
|
out := copyFieldElement(buf, &y)
|
||||||
|
out[31] |= byte(x.IsNegative() << 7)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
var feOne = new(field.Element).One()
|
||||||
|
|
||||||
|
// SetBytes sets v = x, where x is a 32-byte encoding of v. If x does not
|
||||||
|
// represent a valid point on the curve, SetBytes returns nil and an error and
|
||||||
|
// the receiver is unchanged. Otherwise, SetBytes returns v.
|
||||||
|
//
|
||||||
|
// Note that SetBytes accepts all non-canonical encodings of valid points.
|
||||||
|
// That is, it follows decoding rules that match most implementations in
|
||||||
|
// the ecosystem rather than RFC 8032.
|
||||||
|
func (v *Point) SetBytes(x []byte) (*Point, error) {
|
||||||
|
// Specifically, the non-canonical encodings that are accepted are
|
||||||
|
// 1) the ones where the field element is not reduced (see the
|
||||||
|
// (*field.Element).SetBytes docs) and
|
||||||
|
// 2) the ones where the x-coordinate is zero and the sign bit is set.
|
||||||
|
//
|
||||||
|
// Read more at https://hdevalence.ca/blog/2020-10-04-its-25519am,
|
||||||
|
// specifically the "Canonical A, R" section.
|
||||||
|
|
||||||
|
y, err := new(field.Element).SetBytes(x)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("edwards25519: invalid point encoding length")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -x² + y² = 1 + dx²y²
|
||||||
|
// x² + dx²y² = x²(dy² + 1) = y² - 1
|
||||||
|
// x² = (y² - 1) / (dy² + 1)
|
||||||
|
|
||||||
|
// u = y² - 1
|
||||||
|
y2 := new(field.Element).Square(y)
|
||||||
|
u := new(field.Element).Subtract(y2, feOne)
|
||||||
|
|
||||||
|
// v = dy² + 1
|
||||||
|
vv := new(field.Element).Multiply(y2, d)
|
||||||
|
vv = vv.Add(vv, feOne)
|
||||||
|
|
||||||
|
// x = +√(u/v)
|
||||||
|
xx, wasSquare := new(field.Element).SqrtRatio(u, vv)
|
||||||
|
if wasSquare == 0 {
|
||||||
|
return nil, errors.New("edwards25519: invalid point encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the negative square root if the sign bit is set.
|
||||||
|
xxNeg := new(field.Element).Negate(xx)
|
||||||
|
xx = xx.Select(xxNeg, xx, int(x[31]>>7))
|
||||||
|
|
||||||
|
v.x.Set(xx)
|
||||||
|
v.y.Set(y)
|
||||||
|
v.z.One()
|
||||||
|
v.t.Multiply(xx, y) // xy = T / Z
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFieldElement(buf *[32]byte, v *field.Element) []byte {
|
||||||
|
copy(buf[:], v.Bytes())
|
||||||
|
return buf[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversions.
|
||||||
|
|
||||||
|
func (v *projP2) FromP1xP1(p *projP1xP1) *projP2 {
|
||||||
|
v.X.Multiply(&p.X, &p.T)
|
||||||
|
v.Y.Multiply(&p.Y, &p.Z)
|
||||||
|
v.Z.Multiply(&p.Z, &p.T)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projP2) FromP3(p *Point) *projP2 {
|
||||||
|
v.X.Set(&p.x)
|
||||||
|
v.Y.Set(&p.y)
|
||||||
|
v.Z.Set(&p.z)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) fromP1xP1(p *projP1xP1) *Point {
|
||||||
|
v.x.Multiply(&p.X, &p.T)
|
||||||
|
v.y.Multiply(&p.Y, &p.Z)
|
||||||
|
v.z.Multiply(&p.Z, &p.T)
|
||||||
|
v.t.Multiply(&p.X, &p.Y)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) fromP2(p *projP2) *Point {
|
||||||
|
v.x.Multiply(&p.X, &p.Z)
|
||||||
|
v.y.Multiply(&p.Y, &p.Z)
|
||||||
|
v.z.Square(&p.Z)
|
||||||
|
v.t.Multiply(&p.X, &p.Y)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// d is a constant in the curve equation.
|
||||||
|
var d, _ = new(field.Element).SetBytes([]byte{
|
||||||
|
0xa3, 0x78, 0x59, 0x13, 0xca, 0x4d, 0xeb, 0x75,
|
||||||
|
0xab, 0xd8, 0x41, 0x41, 0x4d, 0x0a, 0x70, 0x00,
|
||||||
|
0x98, 0xe8, 0x79, 0x77, 0x79, 0x40, 0xc7, 0x8c,
|
||||||
|
0x73, 0xfe, 0x6f, 0x2b, 0xee, 0x6c, 0x03, 0x52})
|
||||||
|
var d2 = new(field.Element).Add(d, d)
|
||||||
|
|
||||||
|
func (v *projCached) FromP3(p *Point) *projCached {
|
||||||
|
v.YplusX.Add(&p.y, &p.x)
|
||||||
|
v.YminusX.Subtract(&p.y, &p.x)
|
||||||
|
v.Z.Set(&p.z)
|
||||||
|
v.T2d.Multiply(&p.t, d2)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *affineCached) FromP3(p *Point) *affineCached {
|
||||||
|
v.YplusX.Add(&p.y, &p.x)
|
||||||
|
v.YminusX.Subtract(&p.y, &p.x)
|
||||||
|
v.T2d.Multiply(&p.t, d2)
|
||||||
|
|
||||||
|
var invZ field.Element
|
||||||
|
invZ.Invert(&p.z)
|
||||||
|
v.YplusX.Multiply(&v.YplusX, &invZ)
|
||||||
|
v.YminusX.Multiply(&v.YminusX, &invZ)
|
||||||
|
v.T2d.Multiply(&v.T2d, &invZ)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// (Re)addition and subtraction.
|
||||||
|
|
||||||
|
// Add sets v = p + q, and returns v.
|
||||||
|
func (v *Point) Add(p, q *Point) *Point {
|
||||||
|
checkInitialized(p, q)
|
||||||
|
qCached := new(projCached).FromP3(q)
|
||||||
|
result := new(projP1xP1).Add(p, qCached)
|
||||||
|
return v.fromP1xP1(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract sets v = p - q, and returns v.
|
||||||
|
func (v *Point) Subtract(p, q *Point) *Point {
|
||||||
|
checkInitialized(p, q)
|
||||||
|
qCached := new(projCached).FromP3(q)
|
||||||
|
result := new(projP1xP1).Sub(p, qCached)
|
||||||
|
return v.fromP1xP1(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projP1xP1) Add(p *Point, q *projCached) *projP1xP1 {
|
||||||
|
var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element
|
||||||
|
|
||||||
|
YplusX.Add(&p.y, &p.x)
|
||||||
|
YminusX.Subtract(&p.y, &p.x)
|
||||||
|
|
||||||
|
PP.Multiply(&YplusX, &q.YplusX)
|
||||||
|
MM.Multiply(&YminusX, &q.YminusX)
|
||||||
|
TT2d.Multiply(&p.t, &q.T2d)
|
||||||
|
ZZ2.Multiply(&p.z, &q.Z)
|
||||||
|
|
||||||
|
ZZ2.Add(&ZZ2, &ZZ2)
|
||||||
|
|
||||||
|
v.X.Subtract(&PP, &MM)
|
||||||
|
v.Y.Add(&PP, &MM)
|
||||||
|
v.Z.Add(&ZZ2, &TT2d)
|
||||||
|
v.T.Subtract(&ZZ2, &TT2d)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projP1xP1) Sub(p *Point, q *projCached) *projP1xP1 {
|
||||||
|
var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element
|
||||||
|
|
||||||
|
YplusX.Add(&p.y, &p.x)
|
||||||
|
YminusX.Subtract(&p.y, &p.x)
|
||||||
|
|
||||||
|
PP.Multiply(&YplusX, &q.YminusX) // flipped sign
|
||||||
|
MM.Multiply(&YminusX, &q.YplusX) // flipped sign
|
||||||
|
TT2d.Multiply(&p.t, &q.T2d)
|
||||||
|
ZZ2.Multiply(&p.z, &q.Z)
|
||||||
|
|
||||||
|
ZZ2.Add(&ZZ2, &ZZ2)
|
||||||
|
|
||||||
|
v.X.Subtract(&PP, &MM)
|
||||||
|
v.Y.Add(&PP, &MM)
|
||||||
|
v.Z.Subtract(&ZZ2, &TT2d) // flipped sign
|
||||||
|
v.T.Add(&ZZ2, &TT2d) // flipped sign
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projP1xP1) AddAffine(p *Point, q *affineCached) *projP1xP1 {
|
||||||
|
var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element
|
||||||
|
|
||||||
|
YplusX.Add(&p.y, &p.x)
|
||||||
|
YminusX.Subtract(&p.y, &p.x)
|
||||||
|
|
||||||
|
PP.Multiply(&YplusX, &q.YplusX)
|
||||||
|
MM.Multiply(&YminusX, &q.YminusX)
|
||||||
|
TT2d.Multiply(&p.t, &q.T2d)
|
||||||
|
|
||||||
|
Z2.Add(&p.z, &p.z)
|
||||||
|
|
||||||
|
v.X.Subtract(&PP, &MM)
|
||||||
|
v.Y.Add(&PP, &MM)
|
||||||
|
v.Z.Add(&Z2, &TT2d)
|
||||||
|
v.T.Subtract(&Z2, &TT2d)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *projP1xP1) SubAffine(p *Point, q *affineCached) *projP1xP1 {
|
||||||
|
var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element
|
||||||
|
|
||||||
|
YplusX.Add(&p.y, &p.x)
|
||||||
|
YminusX.Subtract(&p.y, &p.x)
|
||||||
|
|
||||||
|
PP.Multiply(&YplusX, &q.YminusX) // flipped sign
|
||||||
|
MM.Multiply(&YminusX, &q.YplusX) // flipped sign
|
||||||
|
TT2d.Multiply(&p.t, &q.T2d)
|
||||||
|
|
||||||
|
Z2.Add(&p.z, &p.z)
|
||||||
|
|
||||||
|
v.X.Subtract(&PP, &MM)
|
||||||
|
v.Y.Add(&PP, &MM)
|
||||||
|
v.Z.Subtract(&Z2, &TT2d) // flipped sign
|
||||||
|
v.T.Add(&Z2, &TT2d) // flipped sign
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doubling.
|
||||||
|
|
||||||
|
func (v *projP1xP1) Double(p *projP2) *projP1xP1 {
|
||||||
|
var XX, YY, ZZ2, XplusYsq field.Element
|
||||||
|
|
||||||
|
XX.Square(&p.X)
|
||||||
|
YY.Square(&p.Y)
|
||||||
|
ZZ2.Square(&p.Z)
|
||||||
|
ZZ2.Add(&ZZ2, &ZZ2)
|
||||||
|
XplusYsq.Add(&p.X, &p.Y)
|
||||||
|
XplusYsq.Square(&XplusYsq)
|
||||||
|
|
||||||
|
v.Y.Add(&YY, &XX)
|
||||||
|
v.Z.Subtract(&YY, &XX)
|
||||||
|
|
||||||
|
v.X.Subtract(&XplusYsq, &v.Y)
|
||||||
|
v.T.Subtract(&ZZ2, &v.Z)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negation.
|
||||||
|
|
||||||
|
// Negate sets v = -p, and returns v.
|
||||||
|
func (v *Point) Negate(p *Point) *Point {
|
||||||
|
checkInitialized(p)
|
||||||
|
v.x.Negate(&p.x)
|
||||||
|
v.y.Set(&p.y)
|
||||||
|
v.z.Set(&p.z)
|
||||||
|
v.t.Negate(&p.t)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns 1 if v is equivalent to u, and 0 otherwise.
|
||||||
|
func (v *Point) Equal(u *Point) int {
|
||||||
|
checkInitialized(v, u)
|
||||||
|
|
||||||
|
var t1, t2, t3, t4 field.Element
|
||||||
|
t1.Multiply(&v.x, &u.z)
|
||||||
|
t2.Multiply(&u.x, &v.z)
|
||||||
|
t3.Multiply(&v.y, &u.z)
|
||||||
|
t4.Multiply(&u.y, &v.z)
|
||||||
|
|
||||||
|
return t1.Equal(&t2) & t3.Equal(&t4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constant-time operations
|
||||||
|
|
||||||
|
// Select sets v to a if cond == 1 and to b if cond == 0.
|
||||||
|
func (v *projCached) Select(a, b *projCached, cond int) *projCached {
|
||||||
|
v.YplusX.Select(&a.YplusX, &b.YplusX, cond)
|
||||||
|
v.YminusX.Select(&a.YminusX, &b.YminusX, cond)
|
||||||
|
v.Z.Select(&a.Z, &b.Z, cond)
|
||||||
|
v.T2d.Select(&a.T2d, &b.T2d, cond)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select sets v to a if cond == 1 and to b if cond == 0.
|
||||||
|
func (v *affineCached) Select(a, b *affineCached, cond int) *affineCached {
|
||||||
|
v.YplusX.Select(&a.YplusX, &b.YplusX, cond)
|
||||||
|
v.YminusX.Select(&a.YminusX, &b.YminusX, cond)
|
||||||
|
v.T2d.Select(&a.T2d, &b.T2d, cond)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0.
|
||||||
|
func (v *projCached) CondNeg(cond int) *projCached {
|
||||||
|
v.YplusX.Swap(&v.YminusX, cond)
|
||||||
|
v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0.
|
||||||
|
func (v *affineCached) CondNeg(cond int) *affineCached {
|
||||||
|
v.YplusX.Swap(&v.YminusX, cond)
|
||||||
|
v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond)
|
||||||
|
return v
|
||||||
|
}
|
349
vendor/filippo.io/edwards25519/extra.go
generated
vendored
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
// Copyright (c) 2021 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package edwards25519
|
||||||
|
|
||||||
|
// This file contains additional functionality that is not included in the
|
||||||
|
// upstream crypto/internal/edwards25519 package.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"filippo.io/edwards25519/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtendedCoordinates returns v in extended coordinates (X:Y:Z:T) where
|
||||||
|
// x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522.
|
||||||
|
func (v *Point) ExtendedCoordinates() (X, Y, Z, T *field.Element) {
|
||||||
|
// This function is outlined to make the allocations inline in the caller
|
||||||
|
// rather than happen on the heap. Don't change the style without making
|
||||||
|
// sure it doesn't increase the inliner cost.
|
||||||
|
var e [4]field.Element
|
||||||
|
X, Y, Z, T = v.extendedCoordinates(&e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) extendedCoordinates(e *[4]field.Element) (X, Y, Z, T *field.Element) {
|
||||||
|
checkInitialized(v)
|
||||||
|
X = e[0].Set(&v.x)
|
||||||
|
Y = e[1].Set(&v.y)
|
||||||
|
Z = e[2].Set(&v.z)
|
||||||
|
T = e[3].Set(&v.t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExtendedCoordinates sets v = (X:Y:Z:T) in extended coordinates where
|
||||||
|
// x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522.
|
||||||
|
//
|
||||||
|
// If the coordinates are invalid or don't represent a valid point on the curve,
|
||||||
|
// SetExtendedCoordinates returns nil and an error and the receiver is
|
||||||
|
// unchanged. Otherwise, SetExtendedCoordinates returns v.
|
||||||
|
func (v *Point) SetExtendedCoordinates(X, Y, Z, T *field.Element) (*Point, error) {
|
||||||
|
if !isOnCurve(X, Y, Z, T) {
|
||||||
|
return nil, errors.New("edwards25519: invalid point coordinates")
|
||||||
|
}
|
||||||
|
v.x.Set(X)
|
||||||
|
v.y.Set(Y)
|
||||||
|
v.z.Set(Z)
|
||||||
|
v.t.Set(T)
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOnCurve(X, Y, Z, T *field.Element) bool {
|
||||||
|
var lhs, rhs field.Element
|
||||||
|
XX := new(field.Element).Square(X)
|
||||||
|
YY := new(field.Element).Square(Y)
|
||||||
|
ZZ := new(field.Element).Square(Z)
|
||||||
|
TT := new(field.Element).Square(T)
|
||||||
|
// -x² + y² = 1 + dx²y²
|
||||||
|
// -(X/Z)² + (Y/Z)² = 1 + d(T/Z)²
|
||||||
|
// -X² + Y² = Z² + dT²
|
||||||
|
lhs.Subtract(YY, XX)
|
||||||
|
rhs.Multiply(d, TT).Add(&rhs, ZZ)
|
||||||
|
if lhs.Equal(&rhs) != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// xy = T/Z
|
||||||
|
// XY/Z² = T/Z
|
||||||
|
// XY = TZ
|
||||||
|
lhs.Multiply(X, Y)
|
||||||
|
rhs.Multiply(T, Z)
|
||||||
|
return lhs.Equal(&rhs) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytesMontgomery converts v to a point on the birationally-equivalent
|
||||||
|
// Curve25519 Montgomery curve, and returns its canonical 32 bytes encoding
|
||||||
|
// according to RFC 7748.
|
||||||
|
//
|
||||||
|
// Note that BytesMontgomery only encodes the u-coordinate, so v and -v encode
|
||||||
|
// to the same value. If v is the identity point, BytesMontgomery returns 32
|
||||||
|
// zero bytes, analogously to the X25519 function.
|
||||||
|
//
|
||||||
|
// The lack of an inverse operation (such as SetMontgomeryBytes) is deliberate:
|
||||||
|
// while every valid edwards25519 point has a unique u-coordinate Montgomery
|
||||||
|
// encoding, X25519 accepts inputs on the quadratic twist, which don't correspond
|
||||||
|
// to any edwards25519 point, and every other X25519 input corresponds to two
|
||||||
|
// edwards25519 points.
|
||||||
|
func (v *Point) BytesMontgomery() []byte {
|
||||||
|
// This function is outlined to make the allocations inline in the caller
|
||||||
|
// rather than happen on the heap.
|
||||||
|
var buf [32]byte
|
||||||
|
return v.bytesMontgomery(&buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Point) bytesMontgomery(buf *[32]byte) []byte {
|
||||||
|
checkInitialized(v)
|
||||||
|
|
||||||
|
// RFC 7748, Section 4.1 provides the bilinear map to calculate the
|
||||||
|
// Montgomery u-coordinate
|
||||||
|
//
|
||||||
|
// u = (1 + y) / (1 - y)
|
||||||
|
//
|
||||||
|
// where y = Y / Z.
|
||||||
|
|
||||||
|
var y, recip, u field.Element
|
||||||
|
|
||||||
|
y.Multiply(&v.y, y.Invert(&v.z)) // y = Y / Z
|
||||||
|
recip.Invert(recip.Subtract(feOne, &y)) // r = 1/(1 - y)
|
||||||
|
u.Multiply(u.Add(feOne, &y), &recip) // u = (1 + y)*r
|
||||||
|
|
||||||
|
return copyFieldElement(buf, &u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultByCofactor sets v = 8 * p, and returns v.
|
||||||
|
func (v *Point) MultByCofactor(p *Point) *Point {
|
||||||
|
checkInitialized(p)
|
||||||
|
result := projP1xP1{}
|
||||||
|
pp := (&projP2{}).FromP3(p)
|
||||||
|
result.Double(pp)
|
||||||
|
pp.FromP1xP1(&result)
|
||||||
|
result.Double(pp)
|
||||||
|
pp.FromP1xP1(&result)
|
||||||
|
result.Double(pp)
|
||||||
|
return v.fromP1xP1(&result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given k > 0, set s = s**(2*i).
|
||||||
|
func (s *Scalar) pow2k(k int) {
|
||||||
|
for i := 0; i < k; i++ {
|
||||||
|
s.Multiply(s, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert sets s to the inverse of a nonzero scalar v, and returns s.
|
||||||
|
//
|
||||||
|
// If t is zero, Invert returns zero.
|
||||||
|
func (s *Scalar) Invert(t *Scalar) *Scalar {
|
||||||
|
// Uses a hardcoded sliding window of width 4.
|
||||||
|
var table [8]Scalar
|
||||||
|
var tt Scalar
|
||||||
|
tt.Multiply(t, t)
|
||||||
|
table[0] = *t
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
table[i+1].Multiply(&table[i], &tt)
|
||||||
|
}
|
||||||
|
// Now table = [t**1, t**3, t**5, t**7, t**9, t**11, t**13, t**15]
|
||||||
|
// so t**k = t[k/2] for odd k
|
||||||
|
|
||||||
|
// To compute the sliding window digits, use the following Sage script:
|
||||||
|
|
||||||
|
// sage: import itertools
|
||||||
|
// sage: def sliding_window(w,k):
|
||||||
|
// ....: digits = []
|
||||||
|
// ....: while k > 0:
|
||||||
|
// ....: if k % 2 == 1:
|
||||||
|
// ....: kmod = k % (2**w)
|
||||||
|
// ....: digits.append(kmod)
|
||||||
|
// ....: k = k - kmod
|
||||||
|
// ....: else:
|
||||||
|
// ....: digits.append(0)
|
||||||
|
// ....: k = k // 2
|
||||||
|
// ....: return digits
|
||||||
|
|
||||||
|
// Now we can compute s roughly as follows:
|
||||||
|
|
||||||
|
// sage: s = 1
|
||||||
|
// sage: for coeff in reversed(sliding_window(4,l-2)):
|
||||||
|
// ....: s = s*s
|
||||||
|
// ....: if coeff > 0 :
|
||||||
|
// ....: s = s*t**coeff
|
||||||
|
|
||||||
|
// This works on one bit at a time, with many runs of zeros.
|
||||||
|
// The digits can be collapsed into [(count, coeff)] as follows:
|
||||||
|
|
||||||
|
// sage: [(len(list(group)),d) for d,group in itertools.groupby(sliding_window(4,l-2))]
|
||||||
|
|
||||||
|
// Entries of the form (k, 0) turn into pow2k(k)
|
||||||
|
// Entries of the form (1, coeff) turn into a squaring and then a table lookup.
|
||||||
|
// We can fold the squaring into the previous pow2k(k) as pow2k(k+1).
|
||||||
|
|
||||||
|
*s = table[1/2]
|
||||||
|
s.pow2k(127 + 1)
|
||||||
|
s.Multiply(s, &table[1/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[9/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[11/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[13/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[15/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[7/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[15/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[5/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[1/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[15/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[15/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[7/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[3/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[11/2])
|
||||||
|
s.pow2k(5 + 1)
|
||||||
|
s.Multiply(s, &table[11/2])
|
||||||
|
s.pow2k(9 + 1)
|
||||||
|
s.Multiply(s, &table[9/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[3/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[3/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[3/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[9/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[7/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[3/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[13/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[7/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[9/2])
|
||||||
|
s.pow2k(3 + 1)
|
||||||
|
s.Multiply(s, &table[15/2])
|
||||||
|
s.pow2k(4 + 1)
|
||||||
|
s.Multiply(s, &table[11/2])
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v.
|
||||||
|
//
|
||||||
|
// Execution time depends only on the lengths of the two slices, which must match.
|
||||||
|
func (v *Point) MultiScalarMult(scalars []*Scalar, points []*Point) *Point {
|
||||||
|
if len(scalars) != len(points) {
|
||||||
|
panic("edwards25519: called MultiScalarMult with different size inputs")
|
||||||
|
}
|
||||||
|
checkInitialized(points...)
|
||||||
|
|
||||||
|
// Proceed as in the single-base case, but share doublings
|
||||||
|
// between each point in the multiscalar equation.
|
||||||
|
|
||||||
|
// Build lookup tables for each point
|
||||||
|
tables := make([]projLookupTable, len(points))
|
||||||
|
for i := range tables {
|
||||||
|
tables[i].FromP3(points[i])
|
||||||
|
}
|
||||||
|
// Compute signed radix-16 digits for each scalar
|
||||||
|
digits := make([][64]int8, len(scalars))
|
||||||
|
for i := range digits {
|
||||||
|
digits[i] = scalars[i].signedRadix16()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap first loop iteration to save computing 16*identity
|
||||||
|
multiple := &projCached{}
|
||||||
|
tmp1 := &projP1xP1{}
|
||||||
|
tmp2 := &projP2{}
|
||||||
|
// Lookup-and-add the appropriate multiple of each input point
|
||||||
|
for j := range tables {
|
||||||
|
tables[j].SelectInto(multiple, digits[j][63])
|
||||||
|
tmp1.Add(v, multiple) // tmp1 = v + x_(j,63)*Q in P1xP1 coords
|
||||||
|
v.fromP1xP1(tmp1) // update v
|
||||||
|
}
|
||||||
|
tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration
|
||||||
|
for i := 62; i >= 0; i-- {
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 2*(prev) in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 2*(prev) in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 4*(prev) in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 4*(prev) in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 8*(prev) in P1xP1 coords
|
||||||
|
tmp2.FromP1xP1(tmp1) // tmp2 = 8*(prev) in P2 coords
|
||||||
|
tmp1.Double(tmp2) // tmp1 = 16*(prev) in P1xP1 coords
|
||||||
|
v.fromP1xP1(tmp1) // v = 16*(prev) in P3 coords
|
||||||
|
// Lookup-and-add the appropriate multiple of each input point
|
||||||
|
for j := range tables {
|
||||||
|
tables[j].SelectInto(multiple, digits[j][i])
|
||||||
|
tmp1.Add(v, multiple) // tmp1 = v + x_(j,i)*Q in P1xP1 coords
|
||||||
|
v.fromP1xP1(tmp1) // update v
|
||||||
|
}
|
||||||
|
tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// VarTimeMultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v.
|
||||||
|
//
|
||||||
|
// Execution time depends on the inputs.
|
||||||
|
func (v *Point) VarTimeMultiScalarMult(scalars []*Scalar, points []*Point) *Point {
|
||||||
|
if len(scalars) != len(points) {
|
||||||
|
panic("edwards25519: called VarTimeMultiScalarMult with different size inputs")
|
||||||
|
}
|
||||||
|
checkInitialized(points...)
|
||||||
|
|
||||||
|
// Generalize double-base NAF computation to arbitrary sizes.
|
||||||
|
// Here all the points are dynamic, so we only use the smaller
|
||||||
|
// tables.
|
||||||
|
|
||||||
|
// Build lookup tables for each point
|
||||||
|
tables := make([]nafLookupTable5, len(points))
|
||||||
|
for i := range tables {
|
||||||
|
tables[i].FromP3(points[i])
|
||||||
|
}
|
||||||
|
// Compute a NAF for each scalar
|
||||||
|
nafs := make([][256]int8, len(scalars))
|
||||||
|
for i := range nafs {
|
||||||
|
nafs[i] = scalars[i].nonAdjacentForm(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
multiple := &projCached{}
|
||||||
|
tmp1 := &projP1xP1{}
|
||||||
|
tmp2 := &projP2{}
|
||||||
|
tmp2.Zero()
|
||||||
|
|
||||||
|
// Move from high to low bits, doubling the accumulator
|
||||||
|
// at each iteration and checking whether there is a nonzero
|
||||||
|
// coefficient to look up a multiple of.
|
||||||
|
//
|
||||||
|
// Skip trying to find the first nonzero coefficent, because
|
||||||
|
// searching might be more work than a few extra doublings.
|
||||||
|
for i := 255; i >= 0; i-- {
|
||||||
|
tmp1.Double(tmp2)
|
||||||
|
|
||||||
|
for j := range nafs {
|
||||||
|
if nafs[j][i] > 0 {
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
tables[j].SelectInto(multiple, nafs[j][i])
|
||||||
|
tmp1.Add(v, multiple)
|
||||||
|
} else if nafs[j][i] < 0 {
|
||||||
|
v.fromP1xP1(tmp1)
|
||||||
|
tables[j].SelectInto(multiple, -nafs[j][i])
|
||||||
|
tmp1.Sub(v, multiple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp2.FromP1xP1(tmp1)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.fromP2(tmp2)
|
||||||
|
return v
|
||||||
|
}
|
420
vendor/filippo.io/edwards25519/field/fe.go
generated
vendored
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
// Copyright (c) 2017 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package field implements fast arithmetic modulo 2^255-19.
|
||||||
|
package field
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math/bits"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Element represents an element of the field GF(2^255-19). Note that this
|
||||||
|
// is not a cryptographically secure group, and should only be used to interact
|
||||||
|
// with edwards25519.Point coordinates.
|
||||||
|
//
|
||||||
|
// This type works similarly to math/big.Int, and all arguments and receivers
|
||||||
|
// are allowed to alias.
|
||||||
|
//
|
||||||
|
// The zero value is a valid zero element.
|
||||||
|
type Element struct {
|
||||||
|
// An element t represents the integer
|
||||||
|
// t.l0 + t.l1*2^51 + t.l2*2^102 + t.l3*2^153 + t.l4*2^204
|
||||||
|
//
|
||||||
|
// Between operations, all limbs are expected to be lower than 2^52.
|
||||||
|
l0 uint64
|
||||||
|
l1 uint64
|
||||||
|
l2 uint64
|
||||||
|
l3 uint64
|
||||||
|
l4 uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskLow51Bits uint64 = (1 << 51) - 1
|
||||||
|
|
||||||
|
var feZero = &Element{0, 0, 0, 0, 0}
|
||||||
|
|
||||||
|
// Zero sets v = 0, and returns v.
|
||||||
|
func (v *Element) Zero() *Element {
|
||||||
|
*v = *feZero
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
var feOne = &Element{1, 0, 0, 0, 0}
|
||||||
|
|
||||||
|
// One sets v = 1, and returns v.
|
||||||
|
func (v *Element) One() *Element {
|
||||||
|
*v = *feOne
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// reduce reduces v modulo 2^255 - 19 and returns it.
|
||||||
|
func (v *Element) reduce() *Element {
|
||||||
|
v.carryPropagate()
|
||||||
|
|
||||||
|
// After the light reduction we now have a field element representation
|
||||||
|
// v < 2^255 + 2^13 * 19, but need v < 2^255 - 19.
|
||||||
|
|
||||||
|
// If v >= 2^255 - 19, then v + 19 >= 2^255, which would overflow 2^255 - 1,
|
||||||
|
// generating a carry. That is, c will be 0 if v < 2^255 - 19, and 1 otherwise.
|
||||||
|
c := (v.l0 + 19) >> 51
|
||||||
|
c = (v.l1 + c) >> 51
|
||||||
|
c = (v.l2 + c) >> 51
|
||||||
|
c = (v.l3 + c) >> 51
|
||||||
|
c = (v.l4 + c) >> 51
|
||||||
|
|
||||||
|
// If v < 2^255 - 19 and c = 0, this will be a no-op. Otherwise, it's
|
||||||
|
// effectively applying the reduction identity to the carry.
|
||||||
|
v.l0 += 19 * c
|
||||||
|
|
||||||
|
v.l1 += v.l0 >> 51
|
||||||
|
v.l0 = v.l0 & maskLow51Bits
|
||||||
|
v.l2 += v.l1 >> 51
|
||||||
|
v.l1 = v.l1 & maskLow51Bits
|
||||||
|
v.l3 += v.l2 >> 51
|
||||||
|
v.l2 = v.l2 & maskLow51Bits
|
||||||
|
v.l4 += v.l3 >> 51
|
||||||
|
v.l3 = v.l3 & maskLow51Bits
|
||||||
|
// no additional carry
|
||||||
|
v.l4 = v.l4 & maskLow51Bits
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sets v = a + b, and returns v.
|
||||||
|
func (v *Element) Add(a, b *Element) *Element {
|
||||||
|
v.l0 = a.l0 + b.l0
|
||||||
|
v.l1 = a.l1 + b.l1
|
||||||
|
v.l2 = a.l2 + b.l2
|
||||||
|
v.l3 = a.l3 + b.l3
|
||||||
|
v.l4 = a.l4 + b.l4
|
||||||
|
// Using the generic implementation here is actually faster than the
|
||||||
|
// assembly. Probably because the body of this function is so simple that
|
||||||
|
// the compiler can figure out better optimizations by inlining the carry
|
||||||
|
// propagation.
|
||||||
|
return v.carryPropagateGeneric()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract sets v = a - b, and returns v.
|
||||||
|
func (v *Element) Subtract(a, b *Element) *Element {
|
||||||
|
// We first add 2 * p, to guarantee the subtraction won't underflow, and
|
||||||
|
// then subtract b (which can be up to 2^255 + 2^13 * 19).
|
||||||
|
v.l0 = (a.l0 + 0xFFFFFFFFFFFDA) - b.l0
|
||||||
|
v.l1 = (a.l1 + 0xFFFFFFFFFFFFE) - b.l1
|
||||||
|
v.l2 = (a.l2 + 0xFFFFFFFFFFFFE) - b.l2
|
||||||
|
v.l3 = (a.l3 + 0xFFFFFFFFFFFFE) - b.l3
|
||||||
|
v.l4 = (a.l4 + 0xFFFFFFFFFFFFE) - b.l4
|
||||||
|
return v.carryPropagate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negate sets v = -a, and returns v.
|
||||||
|
func (v *Element) Negate(a *Element) *Element {
|
||||||
|
return v.Subtract(feZero, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert sets v = 1/z mod p, and returns v.
|
||||||
|
//
|
||||||
|
// If z == 0, Invert returns v = 0.
|
||||||
|
func (v *Element) Invert(z *Element) *Element {
|
||||||
|
// Inversion is implemented as exponentiation with exponent p − 2. It uses the
|
||||||
|
// same sequence of 255 squarings and 11 multiplications as [Curve25519].
|
||||||
|
var z2, z9, z11, z2_5_0, z2_10_0, z2_20_0, z2_50_0, z2_100_0, t Element
|
||||||
|
|
||||||
|
z2.Square(z) // 2
|
||||||
|
t.Square(&z2) // 4
|
||||||
|
t.Square(&t) // 8
|
||||||
|
z9.Multiply(&t, z) // 9
|
||||||
|
z11.Multiply(&z9, &z2) // 11
|
||||||
|
t.Square(&z11) // 22
|
||||||
|
z2_5_0.Multiply(&t, &z9) // 31 = 2^5 - 2^0
|
||||||
|
|
||||||
|
t.Square(&z2_5_0) // 2^6 - 2^1
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
t.Square(&t) // 2^10 - 2^5
|
||||||
|
}
|
||||||
|
z2_10_0.Multiply(&t, &z2_5_0) // 2^10 - 2^0
|
||||||
|
|
||||||
|
t.Square(&z2_10_0) // 2^11 - 2^1
|
||||||
|
for i := 0; i < 9; i++ {
|
||||||
|
t.Square(&t) // 2^20 - 2^10
|
||||||
|
}
|
||||||
|
z2_20_0.Multiply(&t, &z2_10_0) // 2^20 - 2^0
|
||||||
|
|
||||||
|
t.Square(&z2_20_0) // 2^21 - 2^1
|
||||||
|
for i := 0; i < 19; i++ {
|
||||||
|
t.Square(&t) // 2^40 - 2^20
|
||||||
|
}
|
||||||
|
t.Multiply(&t, &z2_20_0) // 2^40 - 2^0
|
||||||
|
|
||||||
|
t.Square(&t) // 2^41 - 2^1
|
||||||
|
for i := 0; i < 9; i++ {
|
||||||
|
t.Square(&t) // 2^50 - 2^10
|
||||||
|
}
|
||||||
|
z2_50_0.Multiply(&t, &z2_10_0) // 2^50 - 2^0
|
||||||
|
|
||||||
|
t.Square(&z2_50_0) // 2^51 - 2^1
|
||||||
|
for i := 0; i < 49; i++ {
|
||||||
|
t.Square(&t) // 2^100 - 2^50
|
||||||
|
}
|
||||||
|
z2_100_0.Multiply(&t, &z2_50_0) // 2^100 - 2^0
|
||||||
|
|
||||||
|
t.Square(&z2_100_0) // 2^101 - 2^1
|
||||||
|
for i := 0; i < 99; i++ {
|
||||||
|
t.Square(&t) // 2^200 - 2^100
|
||||||
|
}
|
||||||
|
t.Multiply(&t, &z2_100_0) // 2^200 - 2^0
|
||||||
|
|
||||||
|
t.Square(&t) // 2^201 - 2^1
|
||||||
|
for i := 0; i < 49; i++ {
|
||||||
|
t.Square(&t) // 2^250 - 2^50
|
||||||
|
}
|
||||||
|
t.Multiply(&t, &z2_50_0) // 2^250 - 2^0
|
||||||
|
|
||||||
|
t.Square(&t) // 2^251 - 2^1
|
||||||
|
t.Square(&t) // 2^252 - 2^2
|
||||||
|
t.Square(&t) // 2^253 - 2^3
|
||||||
|
t.Square(&t) // 2^254 - 2^4
|
||||||
|
t.Square(&t) // 2^255 - 2^5
|
||||||
|
|
||||||
|
return v.Multiply(&t, &z11) // 2^255 - 21
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets v = a, and returns v.
|
||||||
|
func (v *Element) Set(a *Element) *Element {
|
||||||
|
*v = *a
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBytes sets v to x, where x is a 32-byte little-endian encoding. If x is
|
||||||
|
// not of the right length, SetBytes returns nil and an error, and the
|
||||||
|
// receiver is unchanged.
|
||||||
|
//
|
||||||
|
// Consistent with RFC 7748, the most significant bit (the high bit of the
|
||||||
|
// last byte) is ignored, and non-canonical values (2^255-19 through 2^255-1)
|
||||||
|
// are accepted. Note that this is laxer than specified by RFC 8032, but
|
||||||
|
// consistent with most Ed25519 implementations.
|
||||||
|
func (v *Element) SetBytes(x []byte) (*Element, error) {
|
||||||
|
if len(x) != 32 {
|
||||||
|
return nil, errors.New("edwards25519: invalid field element input size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bits 0:51 (bytes 0:8, bits 0:64, shift 0, mask 51).
|
||||||
|
v.l0 = binary.LittleEndian.Uint64(x[0:8])
|
||||||
|
v.l0 &= maskLow51Bits
|
||||||
|
// Bits 51:102 (bytes 6:14, bits 48:112, shift 3, mask 51).
|
||||||
|
v.l1 = binary.LittleEndian.Uint64(x[6:14]) >> 3
|
||||||
|
v.l1 &= maskLow51Bits
|
||||||
|
// Bits 102:153 (bytes 12:20, bits 96:160, shift 6, mask 51).
|
||||||
|
v.l2 = binary.LittleEndian.Uint64(x[12:20]) >> 6
|
||||||
|
v.l2 &= maskLow51Bits
|
||||||
|
// Bits 153:204 (bytes 19:27, bits 152:216, shift 1, mask 51).
|
||||||
|
v.l3 = binary.LittleEndian.Uint64(x[19:27]) >> 1
|
||||||
|
v.l3 &= maskLow51Bits
|
||||||
|
// Bits 204:255 (bytes 24:32, bits 192:256, shift 12, mask 51).
|
||||||
|
// Note: not bytes 25:33, shift 4, to avoid overread.
|
||||||
|
v.l4 = binary.LittleEndian.Uint64(x[24:32]) >> 12
|
||||||
|
v.l4 &= maskLow51Bits
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the canonical 32-byte little-endian encoding of v.
|
||||||
|
func (v *Element) Bytes() []byte {
|
||||||
|
// This function is outlined to make the allocations inline in the caller
|
||||||
|
// rather than happen on the heap.
|
||||||
|
var out [32]byte
|
||||||
|
return v.bytes(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Element) bytes(out *[32]byte) []byte {
|
||||||
|
t := *v
|
||||||
|
t.reduce()
|
||||||
|
|
||||||
|
var buf [8]byte
|
||||||
|
for i, l := range [5]uint64{t.l0, t.l1, t.l2, t.l3, t.l4} {
|
||||||
|
bitsOffset := i * 51
|
||||||
|
binary.LittleEndian.PutUint64(buf[:], l<<uint(bitsOffset%8))
|
||||||
|
for i, bb := range buf {
|
||||||
|
off := bitsOffset/8 + i
|
||||||
|
if off >= len(out) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
out[off] |= bb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns 1 if v and u are equal, and 0 otherwise.
|
||||||
|
func (v *Element) Equal(u *Element) int {
|
||||||
|
sa, sv := u.Bytes(), v.Bytes()
|
||||||
|
return subtle.ConstantTimeCompare(sa, sv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mask64Bits returns 0xffffffff if cond is 1, and 0 otherwise.
|
||||||
|
func mask64Bits(cond int) uint64 { return ^(uint64(cond) - 1) }
|
||||||
|
|
||||||
|
// Select sets v to a if cond == 1, and to b if cond == 0.
|
||||||
|
func (v *Element) Select(a, b *Element, cond int) *Element {
|
||||||
|
m := mask64Bits(cond)
|
||||||
|
v.l0 = (m & a.l0) | (^m & b.l0)
|
||||||
|
v.l1 = (m & a.l1) | (^m & b.l1)
|
||||||
|
v.l2 = (m & a.l2) | (^m & b.l2)
|
||||||
|
v.l3 = (m & a.l3) | (^m & b.l3)
|
||||||
|
v.l4 = (m & a.l4) | (^m & b.l4)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps v and u if cond == 1 or leaves them unchanged if cond == 0, and returns v.
|
||||||
|
func (v *Element) Swap(u *Element, cond int) {
|
||||||
|
m := mask64Bits(cond)
|
||||||
|
t := m & (v.l0 ^ u.l0)
|
||||||
|
v.l0 ^= t
|
||||||
|
u.l0 ^= t
|
||||||
|
t = m & (v.l1 ^ u.l1)
|
||||||
|
v.l1 ^= t
|
||||||
|
u.l1 ^= t
|
||||||
|
t = m & (v.l2 ^ u.l2)
|
||||||
|
v.l2 ^= t
|
||||||
|
u.l2 ^= t
|
||||||
|
t = m & (v.l3 ^ u.l3)
|
||||||
|
v.l3 ^= t
|
||||||
|
u.l3 ^= t
|
||||||
|
t = m & (v.l4 ^ u.l4)
|
||||||
|
v.l4 ^= t
|
||||||
|
u.l4 ^= t
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNegative returns 1 if v is negative, and 0 otherwise.
|
||||||
|
func (v *Element) IsNegative() int {
|
||||||
|
return int(v.Bytes()[0] & 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute sets v to |u|, and returns v.
|
||||||
|
func (v *Element) Absolute(u *Element) *Element {
|
||||||
|
return v.Select(new(Element).Negate(u), u, u.IsNegative())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply sets v = x * y, and returns v.
|
||||||
|
func (v *Element) Multiply(x, y *Element) *Element {
|
||||||
|
feMul(v, x, y)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Square sets v = x * x, and returns v.
|
||||||
|
func (v *Element) Square(x *Element) *Element {
|
||||||
|
feSquare(v, x)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mult32 sets v = x * y, and returns v.
|
||||||
|
func (v *Element) Mult32(x *Element, y uint32) *Element {
|
||||||
|
x0lo, x0hi := mul51(x.l0, y)
|
||||||
|
x1lo, x1hi := mul51(x.l1, y)
|
||||||
|
x2lo, x2hi := mul51(x.l2, y)
|
||||||
|
x3lo, x3hi := mul51(x.l3, y)
|
||||||
|
x4lo, x4hi := mul51(x.l4, y)
|
||||||
|
v.l0 = x0lo + 19*x4hi // carried over per the reduction identity
|
||||||
|
v.l1 = x1lo + x0hi
|
||||||
|
v.l2 = x2lo + x1hi
|
||||||
|
v.l3 = x3lo + x2hi
|
||||||
|
v.l4 = x4lo + x3hi
|
||||||
|
// The hi portions are going to be only 32 bits, plus any previous excess,
|
||||||
|
// so we can skip the carry propagation.
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// mul51 returns lo + hi * 2⁵¹ = a * b.
|
||||||
|
func mul51(a uint64, b uint32) (lo uint64, hi uint64) {
|
||||||
|
mh, ml := bits.Mul64(a, uint64(b))
|
||||||
|
lo = ml & maskLow51Bits
|
||||||
|
hi = (mh << 13) | (ml >> 51)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pow22523 set v = x^((p-5)/8), and returns v. (p-5)/8 is 2^252-3.
|
||||||
|
func (v *Element) Pow22523(x *Element) *Element {
|
||||||
|
var t0, t1, t2 Element
|
||||||
|
|
||||||
|
t0.Square(x) // x^2
|
||||||
|
t1.Square(&t0) // x^4
|
||||||
|
t1.Square(&t1) // x^8
|
||||||
|
t1.Multiply(x, &t1) // x^9
|
||||||
|
t0.Multiply(&t0, &t1) // x^11
|
||||||
|
t0.Square(&t0) // x^22
|
||||||
|
t0.Multiply(&t1, &t0) // x^31
|
||||||
|
t1.Square(&t0) // x^62
|
||||||
|
for i := 1; i < 5; i++ { // x^992
|
||||||
|
t1.Square(&t1)
|
||||||
|
}
|
||||||
|
t0.Multiply(&t1, &t0) // x^1023 -> 1023 = 2^10 - 1
|
||||||
|
t1.Square(&t0) // 2^11 - 2
|
||||||
|
for i := 1; i < 10; i++ { // 2^20 - 2^10
|
||||||
|
t1.Square(&t1)
|
||||||
|
}
|
||||||
|
t1.Multiply(&t1, &t0) // 2^20 - 1
|
||||||
|
t2.Square(&t1) // 2^21 - 2
|
||||||
|
for i := 1; i < 20; i++ { // 2^40 - 2^20
|
||||||
|
t2.Square(&t2)
|
||||||
|
}
|
||||||
|
t1.Multiply(&t2, &t1) // 2^40 - 1
|
||||||
|
t1.Square(&t1) // 2^41 - 2
|
||||||
|
for i := 1; i < 10; i++ { // 2^50 - 2^10
|
||||||
|
t1.Square(&t1)
|
||||||
|
}
|
||||||
|
t0.Multiply(&t1, &t0) // 2^50 - 1
|
||||||
|
t1.Square(&t0) // 2^51 - 2
|
||||||
|
for i := 1; i < 50; i++ { // 2^100 - 2^50
|
||||||
|
t1.Square(&t1)
|
||||||
|
}
|
||||||
|
t1.Multiply(&t1, &t0) // 2^100 - 1
|
||||||
|
t2.Square(&t1) // 2^101 - 2
|
||||||
|
for i := 1; i < 100; i++ { // 2^200 - 2^100
|
||||||
|
t2.Square(&t2)
|
||||||
|
}
|
||||||
|
t1.Multiply(&t2, &t1) // 2^200 - 1
|
||||||
|
t1.Square(&t1) // 2^201 - 2
|
||||||
|
for i := 1; i < 50; i++ { // 2^250 - 2^50
|
||||||
|
t1.Square(&t1)
|
||||||
|
}
|
||||||
|
t0.Multiply(&t1, &t0) // 2^250 - 1
|
||||||
|
t0.Square(&t0) // 2^251 - 2
|
||||||
|
t0.Square(&t0) // 2^252 - 4
|
||||||
|
return v.Multiply(&t0, x) // 2^252 - 3 -> x^(2^252-3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqrtM1 is 2^((p-1)/4), which squared is equal to -1 by Euler's Criterion.
|
||||||
|
var sqrtM1 = &Element{1718705420411056, 234908883556509,
|
||||||
|
2233514472574048, 2117202627021982, 765476049583133}
|
||||||
|
|
||||||
|
// SqrtRatio sets r to the non-negative square root of the ratio of u and v.
|
||||||
|
//
|
||||||
|
// If u/v is square, SqrtRatio returns r and 1. If u/v is not square, SqrtRatio
|
||||||
|
// sets r according to Section 4.3 of draft-irtf-cfrg-ristretto255-decaf448-00,
|
||||||
|
// and returns r and 0.
|
||||||
|
func (r *Element) SqrtRatio(u, v *Element) (R *Element, wasSquare int) {
|
||||||
|
t0 := new(Element)
|
||||||
|
|
||||||
|
// r = (u * v3) * (u * v7)^((p-5)/8)
|
||||||
|
v2 := new(Element).Square(v)
|
||||||
|
uv3 := new(Element).Multiply(u, t0.Multiply(v2, v))
|
||||||
|
uv7 := new(Element).Multiply(uv3, t0.Square(v2))
|
||||||
|
rr := new(Element).Multiply(uv3, t0.Pow22523(uv7))
|
||||||
|
|
||||||
|
check := new(Element).Multiply(v, t0.Square(rr)) // check = v * r^2
|
||||||
|
|
||||||
|
uNeg := new(Element).Negate(u)
|
||||||
|
correctSignSqrt := check.Equal(u)
|
||||||
|
flippedSignSqrt := check.Equal(uNeg)
|
||||||
|
flippedSignSqrtI := check.Equal(t0.Multiply(uNeg, sqrtM1))
|
||||||
|
|
||||||
|
rPrime := new(Element).Multiply(rr, sqrtM1) // r_prime = SQRT_M1 * r
|
||||||
|
// r = CT_SELECT(r_prime IF flipped_sign_sqrt | flipped_sign_sqrt_i ELSE r)
|
||||||
|
rr.Select(rPrime, rr, flippedSignSqrt|flippedSignSqrtI)
|
||||||
|
|
||||||
|
r.Absolute(rr) // Choose the nonnegative square root.
|
||||||
|
return r, correctSignSqrt | flippedSignSqrt
|
||||||
|
}
|
16
vendor/filippo.io/edwards25519/field/fe_amd64.go
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:build amd64 && gc && !purego
|
||||||
|
// +build amd64,gc,!purego
|
||||||
|
|
||||||
|
package field
|
||||||
|
|
||||||
|
// feMul sets out = a * b. It works like feMulGeneric.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func feMul(out *Element, a *Element, b *Element)
|
||||||
|
|
||||||
|
// feSquare sets out = a * a. It works like feSquareGeneric.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func feSquare(out *Element, a *Element)
|
379
vendor/filippo.io/edwards25519/field/fe_amd64.s
generated
vendored
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:build amd64 && gc && !purego
|
||||||
|
// +build amd64,gc,!purego
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func feMul(out *Element, a *Element, b *Element)
|
||||||
|
TEXT ·feMul(SB), NOSPLIT, $0-24
|
||||||
|
MOVQ a+8(FP), CX
|
||||||
|
MOVQ b+16(FP), BX
|
||||||
|
|
||||||
|
// r0 = a0×b0
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ (BX)
|
||||||
|
MOVQ AX, DI
|
||||||
|
MOVQ DX, SI
|
||||||
|
|
||||||
|
// r0 += 19×a1×b4
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 32(BX)
|
||||||
|
ADDQ AX, DI
|
||||||
|
ADCQ DX, SI
|
||||||
|
|
||||||
|
// r0 += 19×a2×b3
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 24(BX)
|
||||||
|
ADDQ AX, DI
|
||||||
|
ADCQ DX, SI
|
||||||
|
|
||||||
|
// r0 += 19×a3×b2
|
||||||
|
MOVQ 24(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 16(BX)
|
||||||
|
ADDQ AX, DI
|
||||||
|
ADCQ DX, SI
|
||||||
|
|
||||||
|
// r0 += 19×a4×b1
|
||||||
|
MOVQ 32(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 8(BX)
|
||||||
|
ADDQ AX, DI
|
||||||
|
ADCQ DX, SI
|
||||||
|
|
||||||
|
// r1 = a0×b1
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ 8(BX)
|
||||||
|
MOVQ AX, R9
|
||||||
|
MOVQ DX, R8
|
||||||
|
|
||||||
|
// r1 += a1×b0
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
MULQ (BX)
|
||||||
|
ADDQ AX, R9
|
||||||
|
ADCQ DX, R8
|
||||||
|
|
||||||
|
// r1 += 19×a2×b4
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 32(BX)
|
||||||
|
ADDQ AX, R9
|
||||||
|
ADCQ DX, R8
|
||||||
|
|
||||||
|
// r1 += 19×a3×b3
|
||||||
|
MOVQ 24(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 24(BX)
|
||||||
|
ADDQ AX, R9
|
||||||
|
ADCQ DX, R8
|
||||||
|
|
||||||
|
// r1 += 19×a4×b2
|
||||||
|
MOVQ 32(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 16(BX)
|
||||||
|
ADDQ AX, R9
|
||||||
|
ADCQ DX, R8
|
||||||
|
|
||||||
|
// r2 = a0×b2
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ 16(BX)
|
||||||
|
MOVQ AX, R11
|
||||||
|
MOVQ DX, R10
|
||||||
|
|
||||||
|
// r2 += a1×b1
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
MULQ 8(BX)
|
||||||
|
ADDQ AX, R11
|
||||||
|
ADCQ DX, R10
|
||||||
|
|
||||||
|
// r2 += a2×b0
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
MULQ (BX)
|
||||||
|
ADDQ AX, R11
|
||||||
|
ADCQ DX, R10
|
||||||
|
|
||||||
|
// r2 += 19×a3×b4
|
||||||
|
MOVQ 24(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 32(BX)
|
||||||
|
ADDQ AX, R11
|
||||||
|
ADCQ DX, R10
|
||||||
|
|
||||||
|
// r2 += 19×a4×b3
|
||||||
|
MOVQ 32(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 24(BX)
|
||||||
|
ADDQ AX, R11
|
||||||
|
ADCQ DX, R10
|
||||||
|
|
||||||
|
// r3 = a0×b3
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ 24(BX)
|
||||||
|
MOVQ AX, R13
|
||||||
|
MOVQ DX, R12
|
||||||
|
|
||||||
|
// r3 += a1×b2
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
MULQ 16(BX)
|
||||||
|
ADDQ AX, R13
|
||||||
|
ADCQ DX, R12
|
||||||
|
|
||||||
|
// r3 += a2×b1
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
MULQ 8(BX)
|
||||||
|
ADDQ AX, R13
|
||||||
|
ADCQ DX, R12
|
||||||
|
|
||||||
|
// r3 += a3×b0
|
||||||
|
MOVQ 24(CX), AX
|
||||||
|
MULQ (BX)
|
||||||
|
ADDQ AX, R13
|
||||||
|
ADCQ DX, R12
|
||||||
|
|
||||||
|
// r3 += 19×a4×b4
|
||||||
|
MOVQ 32(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 32(BX)
|
||||||
|
ADDQ AX, R13
|
||||||
|
ADCQ DX, R12
|
||||||
|
|
||||||
|
// r4 = a0×b4
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ 32(BX)
|
||||||
|
MOVQ AX, R15
|
||||||
|
MOVQ DX, R14
|
||||||
|
|
||||||
|
// r4 += a1×b3
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
MULQ 24(BX)
|
||||||
|
ADDQ AX, R15
|
||||||
|
ADCQ DX, R14
|
||||||
|
|
||||||
|
// r4 += a2×b2
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
MULQ 16(BX)
|
||||||
|
ADDQ AX, R15
|
||||||
|
ADCQ DX, R14
|
||||||
|
|
||||||
|
// r4 += a3×b1
|
||||||
|
MOVQ 24(CX), AX
|
||||||
|
MULQ 8(BX)
|
||||||
|
ADDQ AX, R15
|
||||||
|
ADCQ DX, R14
|
||||||
|
|
||||||
|
// r4 += a4×b0
|
||||||
|
MOVQ 32(CX), AX
|
||||||
|
MULQ (BX)
|
||||||
|
ADDQ AX, R15
|
||||||
|
ADCQ DX, R14
|
||||||
|
|
||||||
|
// First reduction chain
|
||||||
|
MOVQ $0x0007ffffffffffff, AX
|
||||||
|
SHLQ $0x0d, DI, SI
|
||||||
|
SHLQ $0x0d, R9, R8
|
||||||
|
SHLQ $0x0d, R11, R10
|
||||||
|
SHLQ $0x0d, R13, R12
|
||||||
|
SHLQ $0x0d, R15, R14
|
||||||
|
ANDQ AX, DI
|
||||||
|
IMUL3Q $0x13, R14, R14
|
||||||
|
ADDQ R14, DI
|
||||||
|
ANDQ AX, R9
|
||||||
|
ADDQ SI, R9
|
||||||
|
ANDQ AX, R11
|
||||||
|
ADDQ R8, R11
|
||||||
|
ANDQ AX, R13
|
||||||
|
ADDQ R10, R13
|
||||||
|
ANDQ AX, R15
|
||||||
|
ADDQ R12, R15
|
||||||
|
|
||||||
|
// Second reduction chain (carryPropagate)
|
||||||
|
MOVQ DI, SI
|
||||||
|
SHRQ $0x33, SI
|
||||||
|
MOVQ R9, R8
|
||||||
|
SHRQ $0x33, R8
|
||||||
|
MOVQ R11, R10
|
||||||
|
SHRQ $0x33, R10
|
||||||
|
MOVQ R13, R12
|
||||||
|
SHRQ $0x33, R12
|
||||||
|
MOVQ R15, R14
|
||||||
|
SHRQ $0x33, R14
|
||||||
|
ANDQ AX, DI
|
||||||
|
IMUL3Q $0x13, R14, R14
|
||||||
|
ADDQ R14, DI
|
||||||
|
ANDQ AX, R9
|
||||||
|
ADDQ SI, R9
|
||||||
|
ANDQ AX, R11
|
||||||
|
ADDQ R8, R11
|
||||||
|
ANDQ AX, R13
|
||||||
|
ADDQ R10, R13
|
||||||
|
ANDQ AX, R15
|
||||||
|
ADDQ R12, R15
|
||||||
|
|
||||||
|
// Store output
|
||||||
|
MOVQ out+0(FP), AX
|
||||||
|
MOVQ DI, (AX)
|
||||||
|
MOVQ R9, 8(AX)
|
||||||
|
MOVQ R11, 16(AX)
|
||||||
|
MOVQ R13, 24(AX)
|
||||||
|
MOVQ R15, 32(AX)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func feSquare(out *Element, a *Element)
|
||||||
|
TEXT ·feSquare(SB), NOSPLIT, $0-16
|
||||||
|
MOVQ a+8(FP), CX
|
||||||
|
|
||||||
|
// r0 = l0×l0
|
||||||
|
MOVQ (CX), AX
|
||||||
|
MULQ (CX)
|
||||||
|
MOVQ AX, SI
|
||||||
|
MOVQ DX, BX
|
||||||
|
|
||||||
|
// r0 += 38×l1×l4
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
IMUL3Q $0x26, AX, AX
|
||||||
|
MULQ 32(CX)
|
||||||
|
ADDQ AX, SI
|
||||||
|
ADCQ DX, BX
|
||||||
|
|
||||||
|
// r0 += 38×l2×l3
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
IMUL3Q $0x26, AX, AX
|
||||||
|
MULQ 24(CX)
|
||||||
|
ADDQ AX, SI
|
||||||
|
ADCQ DX, BX
|
||||||
|
|
||||||
|
// r1 = 2×l0×l1
|
||||||
|
MOVQ (CX), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 8(CX)
|
||||||
|
MOVQ AX, R8
|
||||||
|
MOVQ DX, DI
|
||||||
|
|
||||||
|
// r1 += 38×l2×l4
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
IMUL3Q $0x26, AX, AX
|
||||||
|
MULQ 32(CX)
|
||||||
|
ADDQ AX, R8
|
||||||
|
ADCQ DX, DI
|
||||||
|
|
||||||
|
// r1 += 19×l3×l3
|
||||||
|
MOVQ 24(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 24(CX)
|
||||||
|
ADDQ AX, R8
|
||||||
|
ADCQ DX, DI
|
||||||
|
|
||||||
|
// r2 = 2×l0×l2
|
||||||
|
MOVQ (CX), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 16(CX)
|
||||||
|
MOVQ AX, R10
|
||||||
|
MOVQ DX, R9
|
||||||
|
|
||||||
|
// r2 += l1×l1
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
MULQ 8(CX)
|
||||||
|
ADDQ AX, R10
|
||||||
|
ADCQ DX, R9
|
||||||
|
|
||||||
|
// r2 += 38×l3×l4
|
||||||
|
MOVQ 24(CX), AX
|
||||||
|
IMUL3Q $0x26, AX, AX
|
||||||
|
MULQ 32(CX)
|
||||||
|
ADDQ AX, R10
|
||||||
|
ADCQ DX, R9
|
||||||
|
|
||||||
|
// r3 = 2×l0×l3
|
||||||
|
MOVQ (CX), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 24(CX)
|
||||||
|
MOVQ AX, R12
|
||||||
|
MOVQ DX, R11
|
||||||
|
|
||||||
|
// r3 += 2×l1×l2
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
IMUL3Q $0x02, AX, AX
|
||||||
|
MULQ 16(CX)
|
||||||
|
ADDQ AX, R12
|
||||||
|
ADCQ DX, R11
|
||||||
|
|
||||||
|
// r3 += 19×l4×l4
|
||||||
|
MOVQ 32(CX), AX
|
||||||
|
IMUL3Q $0x13, AX, AX
|
||||||
|
MULQ 32(CX)
|
||||||
|
ADDQ AX, R12
|
||||||
|
ADCQ DX, R11
|
||||||
|
|
||||||
|
// r4 = 2×l0×l4
|
||||||
|
MOVQ (CX), AX
|
||||||
|
SHLQ $0x01, AX
|
||||||
|
MULQ 32(CX)
|
||||||
|
MOVQ AX, R14
|
||||||
|
MOVQ DX, R13
|
||||||
|
|
||||||
|
// r4 += 2×l1×l3
|
||||||
|
MOVQ 8(CX), AX
|
||||||
|
IMUL3Q $0x02, AX, AX
|
||||||
|
MULQ 24(CX)
|
||||||
|
ADDQ AX, R14
|
||||||
|
ADCQ DX, R13
|
||||||
|
|
||||||
|
// r4 += l2×l2
|
||||||
|
MOVQ 16(CX), AX
|
||||||
|
MULQ 16(CX)
|
||||||
|
ADDQ AX, R14
|
||||||
|
ADCQ DX, R13
|
||||||
|
|
||||||
|
// First reduction chain
|
||||||
|
MOVQ $0x0007ffffffffffff, AX
|
||||||
|
SHLQ $0x0d, SI, BX
|
||||||
|
SHLQ $0x0d, R8, DI
|
||||||
|
SHLQ $0x0d, R10, R9
|
||||||
|
SHLQ $0x0d, R12, R11
|
||||||
|
SHLQ $0x0d, R14, R13
|
||||||
|
ANDQ AX, SI
|
||||||
|
IMUL3Q $0x13, R13, R13
|
||||||
|
ADDQ R13, SI
|
||||||
|
ANDQ AX, R8
|
||||||
|
ADDQ BX, R8
|
||||||
|
ANDQ AX, R10
|
||||||
|
ADDQ DI, R10
|
||||||
|
ANDQ AX, R12
|
||||||
|
ADDQ R9, R12
|
||||||
|
ANDQ AX, R14
|
||||||
|
ADDQ R11, R14
|
||||||
|
|
||||||
|
// Second reduction chain (carryPropagate)
|
||||||
|
MOVQ SI, BX
|
||||||
|
SHRQ $0x33, BX
|
||||||
|
MOVQ R8, DI
|
||||||
|
SHRQ $0x33, DI
|
||||||
|
MOVQ R10, R9
|
||||||
|
SHRQ $0x33, R9
|
||||||
|
MOVQ R12, R11
|
||||||
|
SHRQ $0x33, R11
|
||||||
|
MOVQ R14, R13
|
||||||
|
SHRQ $0x33, R13
|
||||||
|
ANDQ AX, SI
|
||||||
|
IMUL3Q $0x13, R13, R13
|
||||||
|
ADDQ R13, SI
|
||||||
|
ANDQ AX, R8
|
||||||
|
ADDQ BX, R8
|
||||||
|
ANDQ AX, R10
|
||||||
|
ADDQ DI, R10
|
||||||
|
ANDQ AX, R12
|
||||||
|
ADDQ R9, R12
|
||||||
|
ANDQ AX, R14
|
||||||
|
ADDQ R11, R14
|
||||||
|
|
||||||
|
// Store output
|
||||||
|
MOVQ out+0(FP), AX
|
||||||
|
MOVQ SI, (AX)
|
||||||
|
MOVQ R8, 8(AX)
|
||||||
|
MOVQ R10, 16(AX)
|
||||||
|
MOVQ R12, 24(AX)
|
||||||
|
MOVQ R14, 32(AX)
|
||||||
|
RET
|
12
vendor/filippo.io/edwards25519/field/fe_amd64_noasm.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !amd64 || !gc || purego
|
||||||
|
// +build !amd64 !gc purego
|
||||||
|
|
||||||
|
package field
|
||||||
|
|
||||||
|
func feMul(v, x, y *Element) { feMulGeneric(v, x, y) }
|
||||||
|
|
||||||
|
func feSquare(v, x *Element) { feSquareGeneric(v, x) }
|
16
vendor/filippo.io/edwards25519/field/fe_arm64.go
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) 2020 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build arm64 && gc && !purego
|
||||||
|
// +build arm64,gc,!purego
|
||||||
|
|
||||||
|
package field
|
||||||
|
|
||||||
|
//go:noescape
|
||||||
|
func carryPropagate(v *Element)
|
||||||
|
|
||||||
|
func (v *Element) carryPropagate() *Element {
|
||||||
|
carryPropagate(v)
|
||||||
|
return v
|
||||||
|
}
|
42
vendor/filippo.io/edwards25519/field/fe_arm64.s
generated
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (c) 2020 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build arm64 && gc && !purego
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// carryPropagate works exactly like carryPropagateGeneric and uses the
|
||||||
|
// same AND, ADD, and LSR+MADD instructions emitted by the compiler, but
|
||||||
|
// avoids loading R0-R4 twice and uses LDP and STP.
|
||||||
|
//
|
||||||
|
// See https://golang.org/issues/43145 for the main compiler issue.
|
||||||
|
//
|
||||||
|
// func carryPropagate(v *Element)
|
||||||
|
TEXT ·carryPropagate(SB),NOFRAME|NOSPLIT,$0-8
|
||||||
|
MOVD v+0(FP), R20
|
||||||
|
|
||||||
|
LDP 0(R20), (R0, R1)
|
||||||
|
LDP 16(R20), (R2, R3)
|
||||||
|
MOVD 32(R20), R4
|
||||||
|
|
||||||
|
AND $0x7ffffffffffff, R0, R10
|
||||||
|
AND $0x7ffffffffffff, R1, R11
|
||||||
|
AND $0x7ffffffffffff, R2, R12
|
||||||
|
AND $0x7ffffffffffff, R3, R13
|
||||||
|
AND $0x7ffffffffffff, R4, R14
|
||||||
|
|
||||||
|
ADD R0>>51, R11, R11
|
||||||
|
ADD R1>>51, R12, R12
|
||||||
|
ADD R2>>51, R13, R13
|
||||||
|
ADD R3>>51, R14, R14
|
||||||
|
// R4>>51 * 19 + R10 -> R10
|
||||||
|
LSR $51, R4, R21
|
||||||
|
MOVD $19, R22
|
||||||
|
MADD R22, R10, R21, R10
|
||||||
|
|
||||||
|
STP (R10, R11), 0(R20)
|
||||||
|
STP (R12, R13), 16(R20)
|
||||||
|
MOVD R14, 32(R20)
|
||||||
|
|
||||||
|
RET
|
12
vendor/filippo.io/edwards25519/field/fe_arm64_noasm.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) 2021 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !arm64 || !gc || purego
|
||||||
|
// +build !arm64 !gc purego
|
||||||
|
|
||||||
|
package field
|
||||||
|
|
||||||
|
func (v *Element) carryPropagate() *Element {
|
||||||
|
return v.carryPropagateGeneric()
|
||||||
|
}
|